V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
MakHoCheung
V2EX  ›  问与答

关于 SwiftUI 的 MVVM

  •  
  •   MakHoCheung · 2022-06-03 10:19:56 +08:00 · 2509 次点击
    这是一个创建于 910 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本人对 MVVM 不熟,看了资料只知道概念并没有透彻的理解。 按我之前学习的理解,一个 ObservableObject 是一个 ViewModel,发布订阅的是 State(网上讲的都是这种),但是当 State 里面的数据也需要双向绑定的时候(传给子 View),发布订阅就变成了另外的 ViewModel,这里面是不是有啥问题。比如下面 MainViewModel,我是不是应该发布的是 users 而不是 userViewModels

    class MainViewModel: ObservableObject {
        @Published var userViewModels: [UserViewModel] = []
    }
    
    class UserViewModel: Identifiable, ObservableObject {
        @Published var name = ""
        let id = UUID()
    }
    
    第 1 条附言  ·  2022-06-03 12:26:13 +08:00
    “但是当 State 里面的数据也需要双向绑定的时候(传给子 View )”,这里改成 State 里面数据需要被修改,比如代码中的 user.name


    我疑问的是我的代码这么做没了 User 这个 Model ,多了个 UserViewModel 这个 ViewModel ,这是不是不符合 MVVM
    15 条回复    2022-06-03 21:56:06 +08:00
    cardioio
        1
    cardioio  
       2022-06-03 11:42:10 +08:00   ❤️ 1
    粗浅的说说,等大佬拍砖指点。

    M 是数据结构,可以有多个,比如你这里可以把 user 定义为一个 struct ,我认为最好不要写成 vm
    V 是 View
    VM 是一个 class 用来做数据的处理,连接 m 和 v ,有点类似于 vue 的 useXXX

    vm 导入到根视图或者需要的页面,然后传到子视图,也可以用 environment 全局,可以调用 vm 中的 @published 变量
    你可能有点误解双向绑定,SwiftUI 中双向绑定是用 @binding
    weiwoxinyou
        2
    weiwoxinyou  
       2022-06-03 12:06:33 +08:00 via Android
    在 react 里面要实现这个变化是通过 props, 每个组件只维护自身的状态,来自上级的状态应该交给上级自己维护
    MakHoCheung
        3
    MakHoCheung  
    OP
       2022-06-03 12:10:49 +08:00
    @cardioio 我疑问的是如果要改 user 的 name 这种情况呢,User 就只能是 Class 了,这个时候算不算是 UserViewModel
    agagega
        4
    agagega  
       2022-06-03 12:11:13 +08:00
    我没能太理解你的问题。

    @ ObservedObject 和 @ State 标记都是用来做数据绑定的,View 里如果引用到了这些标记的变量,当它们改变时,View 也会对应更新。

    之所以要区分 ObservedObject 和 State ,是因为 Swift 不同于 JS ,是一个严格区分值语义和对象语义的语言。Swift 中,struct 和大部分没有 NS 前缀的内置类型都是值语义,即任何一部分被修改了都会视为整个对象被修改;而 class 和 Foundation 里 NS 开头的类型都是对象语义,对它们属性的修改并不会被视为对整个对象的修改。

    因为 MVVM 的核心就是追踪绑定数据的改变,所以 SwiftUI 必须区别对待值语义变量和对象语义变量。对值语义用 @ State ,任何修改都会简单触发 View 重新渲染;对对象语义用 @ ObservedObject ,被它修饰的类型都要满足 ObservableObject 这个协议,这些类型中任何被 @ Published 修饰的成员发生修改,整个对象就会触发 objectWillChange 事件。

    这和 ViewModel 本身没有关系,只是因为 ViewModel 本来就是我们自己定义出来封装事件逻辑的模块,所以通常会实现为 class ,然后加上 @ ObservedObject 做修饰。

    SwiftUI 里还有个修饰符叫 @ StateObject ,也是修饰对象语义的,和 ObservedObject 的区别在于后者不会维护对象的生命周期,而 StateObject 在生命周期上和 State 类似,由当前组件来维护。
    MakHoCheung
        5
    MakHoCheung  
    OP
       2022-06-03 12:17:56 +08:00
    @agagega 我的问题其实是基于怎么在 SwiftUI 上使用 MVVM ,如果 ObservableObject 是 MVVM 里面的 ViewModel ,那么 @Published 修饰的成员变量是 Model 。但是有一种场景比如我要修改用户名,我把 @Published 修饰的成员变量弄成了 ViewModel ,我问的是我这么做是不是不对
    cardioio
        6
    cardioio  
       2022-06-03 12:20:05 +08:00
    @MakHoCheung 要改 name ,很简单啊,在 UserViewModel class 里定义方法就行了。调用的时候在相应的 view 声明实例化就行了。
    MakHoCheung
        7
    MakHoCheung  
    OP
       2022-06-03 12:22:15 +08:00
    @cardioio 是的,这个时候问题就来了,现在没有了 Model 了。UserViewModel 承担了 ViewModel 和 Model 的角色,这么做感觉不是 MVVM 了
    agagega
        8
    agagega  
       2022-06-03 12:34:23 +08:00
    @MakHoCheung
    SwiftUI 有 @ Binding 这个东西,如果你的 User 是 struct 并且 user 是 @ State ,那可以直接$user 传给子组件,子组件 @ Binding user: User 。这种情况因为都是值语义所以不需要定义 ViewModel 了
    agagega
        9
    agagega  
       2022-06-03 12:35:52 +08:00
    @agagega
    复杂的情况下你可以自己定义 Binding ,@ Binding 和$只是语法糖,Binding 的本质就是一个封装了 getter 和 setter 的对象: https://developer.apple.com/documentation/swiftui/binding
    jackyin
        10
    jackyin  
       2022-06-03 13:13:47 +08:00
    @MakHoCheung

    首先,我想说句正确的废话,你之所以没有 model ,是因为你没有定义 Model 。

    你直接把 User 的 name 属性放到了 ViewModel 里了,你这样做有个问题就是当你只想使用 Model 而不是 ViewModel 的时候怎么办?

    所以,你为什么不先定义一个 User 作为 Model ,再把这个 User 作为一个 @Published 属性放到 UserViewModel 里呢?

    另外,要灵活,我记得笑傲江湖里令狐冲被田伯光打掉了剑,于是不能用剑刺了,风清扬提醒他为什么刺剑一定要用剑呢,手指也可以作为剑。回到你的疑问里,你为什么一定纠结于 Model 一定要是个 struct 呢,简单的 Int 或者 String 也可以视作 Model 来使用,而且很常见,比如可以定义一些全局的参数放到一个单例 ViewModel 里,就很方便使用。
    hguandl
        11
    hguandl  
       2022-06-03 13:44:33 +08:00
    不知道 OP 是否看了 WWDC19 的演讲“通过 SwiftUI 的数据流”,这个是最初 SwiftUI 发布时苹果官方对于数据流的介绍。如果没有看过建议补一下 https://developer.apple.com/wwdc19/226 。苹果 WWDC 里的演讲虽然代码不多,但是概念讲解很生动。我认为这应该是学习 Swift 时选择的第一手资料,然后再去 hackingwithswift 等地方学习有经验开发者总结的教程。

    上面链接里的教程由于是最早期的版本,个别 API 存在一些变动。比如里面提到的 BindableObject 已经更名为 ObservedObject ,@Published 属性引入后也不需要像视频里那样手动写更新了。

    Apple Developer 里的学习资料很多,而且近几年的演讲视频都配了中文字幕很不错。不过因为一些术语也翻译成了中文,搜索起来有点麻烦。
    wooi
        12
    wooi  
       2022-06-03 15:12:47 +08:00
    你的 User 或者 Name 都是 Model ,ViewModel 承载着 Model 计算逻辑,例如显示名字 View 要订阅 Name View 点击触发 ViewModel 内的计算逻辑,逻辑执行完毕之后更新被观察到属性,一旦 Name 值发生变化自动刷新你名字 View
    MakHoCheung
        13
    MakHoCheung  
    OP
       2022-06-03 19:53:49 +08:00
    @jackyin

    // View

    struct UsersView: View {
    @StateObject var usersViewModel = UsersViewModel()

    var body: some View {
    VStack {
    ForEach(usersViewModel.users) {
    UserView(user: $0)
    }
    }
    .onAppear {
    usersViewModel.initUsers()
    }
    }
    }

    struct UserView: View {
    @StateObject var user: User

    var body: some View {
    VStack {
    HStack {
    Text(user.name)
    Button("改名") {
    user.changeName(newName: "小明")
    }
    }
    }
    }
    }

    // ViewModel

    class UsersViewModel: ObservableObject {
    @Published var users = [User]()

    func initUsers() {
    users.append(User())
    users.append(User())
    users.append(User())
    }
    }

    // Model

    class User: ObservableObject, Identifiable {
    @Published private(set) var name = "待改名"
    let id = UUID()

    func changeName(newName: String) {
    name = newName
    }
    }


    我这个算 MVVM 吗。我这里把 User 定位为 Model 是对的吗
    hocgin
        14
    hocgin  
       2022-06-03 20:52:22 +08:00 via iPhone
    User 应该是纯粹的数据结构
    jackyin
        15
    jackyin  
       2022-06-03 21:56:06 +08:00
    @MakHoCheung
    可以的,就像 14 楼说的,Model 其实就是数据结构,所以 User 可以看作是 Model ,然后你继承了 ObservableObject ,User 就又可以视作 ViewModel 了。即 User 既为 Model ,又为 ViewModel 。

    另外,我觉得 UsersViewModel 是非必要的,直接在 UsersView 里定义 @State var users = [User]()即可。

    我也是新学的 SwiftUI ,我是做 php 的哈哈哈,昨天刚上架一个背单词的 app ,叫今日背单词,也是 SwiftUI 做的,后台 api 用的 golang ,一起探讨学习吧,好多效果我也还实现不了。

    https://apps.apple.com/cn/app/今日背单词 /id1619751017

    ------------------------------------------------------------

    import SwiftUI

    struct UsersView: View {
    @State var users = [User]()

    var body: some View {
    VStack {
    ForEach(users) {
    UserView(user: $0)
    }
    }
    .onAppear {

    users.append(User())
    users.append(User())
    users.append(User())

    }
    }
    }

    struct UserView: View {
    @StateObject var user: User

    var body: some View {
    VStack {
    HStack {
    Text(user.name)
    Button("改名") {
    user.changeName(newName: "小明")
    }
    }
    }
    }
    }

    // Model
    class User: ObservableObject, Identifiable {
    @Published private(set) var name = "待改名"
    let id = UUID()

    func changeName(newName: String) {
    name = newName
    }
    }
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3557 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 04:30 · PVG 12:30 · LAX 20:30 · JFK 23:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.