之前做前端开发,最近写用 SwiftUI 写 macOS App, SwiftUI是声明式UI 框架,借鉴了很多web 前端的技术,也实现了组件化,MVVM架构,reactive 数据响应,但是swift 毕竟没有 JavaScript 灵活,SwiftUI 的一些写法和web 端有区别,今天总结一些 web 端组件话长讨论的 视图组件之间的 通讯往问题。
视图/组件 之间的通讯,大致可以分为三类,父视图传递消息给子视图、子视图转递消息给父视图、多层级不同分支的视图之间消息传递
父传子视图传递消息
父视图传子视图,在SwiftUI中,就是子视图声明属性变量,父视图用的时候代上参数。这里可以理解为 Vue 组件的 props,但是使用方式有区别。
struct ParentView: View {
@State private var message = "init state"
var body: some View {
VStack {
Text("Parent View: \(message)")
Button("send message") {
// 在父视图中改变 message,子视图也跟着更新
message = "message from Parent View"
}
// 传递给子视图 message
ChildView(message: message)
}
.border(Color.red, width: 2)
.padding()
}
}
struct ChildView: View {
var message: String
var body: some View {
VStack {
Text("Child View: \(message)")
}
.padding()
.border(Color.black, width: 1)
}
}
子视图传递消息给父视图
在 web框架中,Vue 子组件通过事件传递消息给父组件, react 通过Callback 函数完成子传父消息通信。SwiftUI中有三种方式,分别是 @Binding ,Closure 回调函数,共享observable 数据。
@Binding
binding 的属性,在子视图中改变,会修改父视图中的数据,在父视图中使用的时候要用 $+<变量名>。 这种方式不好的地方,父视图无法根据子视图的改变做出其他反应,存在缺陷,没有Vue 组件中的自定义事件传递消息好理解。
struct ParentView: View {
@State private var message = "init state"
var body: some View {
VStack {
Text("Parent View: \(message)")
// 传递给子视图 message, 这里添加了¥前缀
ChildView(message: $message)
}
.border(Color.red, width: 2)
.padding()
}
}
struct ChildView: View {
@Binding var message: String
var body: some View {
VStack {
Text("Child View: \(message)")
Button("send message") {
message = "message from child View"
}
}
.padding()
.border(Color.black, width: 1)
}
}
使用回调(Closure)
这种方式类似react 中的 Callback 函数,父视图可以在接收到子视图的后做其他操作。
struct ParentView: View {
@State private var message: String = "Hello from Parent"
var body: some View {
VStack {
Text("Parent View: \(message)")
// 使用时添加闭包
ChildView { newMessage in
message = newMessage
}
}
.padding()
}
}
struct ChildView: View {
var updateMessage: (String) -> Void
var body: some View {
VStack {
Text("Child View")
Button("Send Message to Parent") {
// 需要传递消息是,调用闭包函数
updateMessage("Hello from Child using Closure")
}
}
.padding()
}
}
效果:
共享 Observable 数据
通过共享的Observable 数据,不同视图之间的数据改变,都会影响到其他视图,这有点web 框架 状态管理工具 pinia 的味道。
struct ParentView: View {
@State private var sharedData = SharedData()
var body: some View {
VStack {
Text("Parent View: \(sharedData.message)")
ChildView(sharedData: sharedData)
}
.onChange(of: sharedData.message) { newValue in
// 当 sharedData.message 发生变化时,打印新值
print("message value changed to \(newValue)")
}
.padding()
}
}
struct ChildView: View {
var sharedData: SharedData
var body: some View {
VStack {
Text("Child View--- \(sharedData.message)")
Button("Send Message to Parent") {
sharedData.message = " Message from child View"
}
}
.padding()
}
}
// 定义共享数据类,符合 Observable 协议
@Observable
class SharedData {
var message: String = "Hello, World!"
}
效果如下:
跨视图级别的消息传
在多层级消息传递上,Vue 提供了 Inject/Provide, react 有 context ,SwiftUI 有环境变量。
使用 environment() 方法为全局注入全局对象,在任何层级的子视图中都能通过@Environment(AppData.self) 引入全局的对像, 在任何子视图中修改,其他用到该全局对象的视图视图也跟着变化。这种方式有点依赖注入的味道,和 Vue 提供了 Inject/Provide, react 有 context
几乎是一样的。
struct ParentView: View {
@Environment(AppData.self) private var appData: AppData
var body: some View {
VStack {
Text("Parent View: \(appData.userName)")
// 这里没有传递参数
ChildView()
}
.border(Color.red, width: 2)
.padding()
}
}
struct ChildView: View {
@Environment(AppData.self) private var appData: AppData
var body: some View {
VStack {
Text("Child View--- \(appData.userName)")
Button("rename usename") {
appData.userName = "from child view"
}
}
.padding()
.background(Color.gray)
}
}
// App.swift
import SwiftUI
import Observation
@main
struct DemoApp: App {
var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appData)
}
}
}
@Observable
class AppData {
var userName: String = "User"
}
效果如下:
总结
从前端框架看 SwiftUI View,三种web 组件常用的通信方式,SwiftUI 也有对应的方法,但总的来说,没有web 框架 接口设计的好用,@binding只能用作修改父视图的参数值, 闭包写起来有点啰唆没有事件方便易于理解,这可能跟 swift 和JavaScript 的语言特性有关。另外 MacOS 14 和iOS 17 引入了 Observation来做简单的视图 状态管理,但复杂的场景中 依然需要Combine,导致用户有很多的选项,但容易混淆。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。