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

关于 SwiftUI 的 MVVM

  •  
  •   MakHoCheung · 126 天前 · 1388 次点击
    这是一个创建于 126 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本人对 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 条附言  ·  126 天前
    “但是当 State 里面的数据也需要双向绑定的时候(传给子 View )”,这里改成 State 里面数据需要被修改,比如代码中的 user.name


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

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

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

    @ 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
       126 天前
    @agagega 我的问题其实是基于怎么在 SwiftUI 上使用 MVVM ,如果 ObservableObject 是 MVVM 里面的 ViewModel ,那么 @Published 修饰的成员变量是 Model 。但是有一种场景比如我要修改用户名,我把 @Published 修饰的成员变量弄成了 ViewModel ,我问的是我这么做是不是不对
    cardioio
        6
    cardioio  
       126 天前
    @MakHoCheung 要改 name ,很简单啊,在 UserViewModel class 里定义方法就行了。调用的时候在相应的 view 声明实例化就行了。
    MakHoCheung
        7
    MakHoCheung  
    OP
       126 天前
    @cardioio 是的,这个时候问题就来了,现在没有了 Model 了。UserViewModel 承担了 ViewModel 和 Model 的角色,这么做感觉不是 MVVM 了
    agagega
        8
    agagega  
       126 天前
    @MakHoCheung
    SwiftUI 有 @ Binding 这个东西,如果你的 User 是 struct 并且 user 是 @ State ,那可以直接$user 传给子组件,子组件 @ Binding user: User 。这种情况因为都是值语义所以不需要定义 ViewModel 了
    agagega
        9
    agagega  
       126 天前
    @agagega
    复杂的情况下你可以自己定义 Binding ,@ Binding 和$只是语法糖,Binding 的本质就是一个封装了 getter 和 setter 的对象: https://developer.apple.com/documentation/swiftui/binding
    jackyin
        10
    jackyin  
       126 天前
    @MakHoCheung

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

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

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

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

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

    Apple Developer 里的学习资料很多,而且近几年的演讲视频都配了中文字幕很不错。不过因为一些术语也翻译成了中文,搜索起来有点麻烦。
    wooi
        12
    wooi  
       126 天前
    你的 User 或者 Name 都是 Model ,ViewModel 承载着 Model 计算逻辑,例如显示名字 View 要订阅 Name View 点击触发 ViewModel 内的计算逻辑,逻辑执行完毕之后更新被观察到属性,一旦 Name 值发生变化自动刷新你名字 View
    MakHoCheung
        13
    MakHoCheung  
    OP
       125 天前
    @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  
       125 天前 via iPhone
    User 应该是纯粹的数据结构
    jackyin
        15
    jackyin  
       125 天前
    @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   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2469 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 235ms · UTC 09:04 · PVG 17:04 · LAX 02:04 · JFK 05:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.