之前做前端开发,最近写用 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)
    }
}

iShot_2024-08-26_09.48.10.gif

使用回调(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()
    }
}

效果:

iShot_2024-08-26_09.56.41.gif

共享 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!"
}

效果如下:

iShot_2024-08-26_10.07.00.gif

跨视图级别的消息传

在多层级消息传递上,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"
}

效果如下:

iShot_2024-08-26_10.21.27.gif

总结

从前端框架看 SwiftUI View,三种web 组件常用的通信方式,SwiftUI 也有对应的方法,但总的来说,没有web 框架 接口设计的好用,@binding只能用作修改父视图的参数值, 闭包写起来有点啰唆没有事件方便易于理解,这可能跟 swift 和JavaScript 的语言特性有关。另外 MacOS 14 和iOS 17 引入了 Observation来做简单的视图 状态管理,但复杂的场景中 依然需要Combine,导致用户有很多的选项,但容易混淆。


today
906 声望41 粉丝