【问题标题】:How should the viewModel object be declared in SwiftUI?在 SwiftUI 中应该如何声明 viewModel 对象?
【发布时间】:2020-12-25 09:14:04
【问题描述】:

我正在尝试使用在我的视图结构中声明为@ObservedObject 的@ObservableObject viewModel。问题是,当 viewModel 更改它的“域”属性 @Published var 时,UI 不会更新。此外,我更改了在 init() {} 中调用的 getDomains() 函数中的域。看起来它被调用了两次,为什么会这样?这是我的 viewModel 代码:

import Combine

class DomainsViewModel: ObservableObject {

    @Published var domains: [InterestDomain] = [InterestDomain]()
    
    init() {
        self.getDomains { (response) in
            print(response)
        }
    }
    
    func getDomains(completion: @escaping (Bool) -> Void) {
        NetworkEngine.shared.appNetwork.getInterestDomains { result in
            self.domains.removeAll()
            switch result {
            case .success(let domainsOfInterest):
                if let domainsList = domainsOfInterest {
                    self.domains = domainsList
                }
                completion(true)
            case .failure(_):
                completion(false)
            }
        }
    }

}

视图代码: 进口基金会 导入 SwiftUI 导入合并

struct DomainsOfInterestView: View {

    @ObservedObject var viewModel: DomainsViewModel = DomainsViewModel()
    
    @State var isActive = true
    
    var body: some View {
            VStack(alignment: .center) {
                HStack {
                    Text("Choose domains of interest for your profile")
                        .font(.headline)
                    Spacer()
                }.padding(.bottom, 16)
                
                ForEach(viewModel.domains.indices) { index in
                    DomainOfInterestElement(isActive: self.$isActive, domain: self.viewModel.domains[index].name)
                }

                OrangeButton(action: {
                }) {
                    Text("Save")
                }.padding(.top, 30)
                    .padding([.leading, .trailing], 30)
                Spacer()
            }.padding([.leading, .trailing], 12)
            .navigationBarTitle("Domains of interest")
    }
}

struct DomainsOfInterestView_Previews: PreviewProvider {
    static var previews: some View {
    DomainsOfInterestView()
    }
}

struct DomainOfInterestElement: View {
    @Binding var isActive: Bool
    var domain: String
    var body: some View {
        Button(action: {
            self.isActive.toggle()
        }) {
            VStack {
                Divider().padding(.bottom, 12)
                HStack {
                    checkBoxView()
                        .frame(width: 36, height: 36)
                    textView().font(.custom("SFProDisplay-Regular", size: 16))
                    Spacer()
                }
            }
        }
    }
    
    func checkBoxView() -> Image {
        switch isActive {
        case true:
            return Image(uiImage: #imageLiteral(resourceName: "check-box-active-2-1")).renderingMode(.original)
        case false:
            return Image(uiImage: #imageLiteral(resourceName: "check-box-active-1")).renderingMode(.original)
        }
    }
    func textView() -> Text {
        switch isActive {
        case true:
            return Text(domain)
                .foregroundColor(.black)
        case false:
            return Text(domain)
                .foregroundColor(Color.orGrayColor)
        }
    }
}

谁能帮帮我?谢谢。

【问题讨论】:

  • 除了下面的答案,您可能需要小心处理多个线程。我看不到 getInterestDomains 的代码,但它可能在后台线程中回调。然后在后台线程中更改已发布的 var 可能会导致发生未定义的事情(例如视图未按应有的方式更新)。您可能需要分派回主队列。这是在视图中不初始化 viewModel 的补充,除非您使用 @StateObject。

标签: swift swiftui reactive-programming combine


【解决方案1】:

DomainsOfInterestView 在每次更新其视图模型时重新创建。并且每次都使用新的 DomainsViewModel 实例初始化视图模型属性。新的视图模型会将domains 属性设置为[InterestDomain](),并且将再次调用self.getDomains

首先,正如@youjin 在对上一篇文章的评论中所写,最好将 viewModel 注入到初始化程序中的DomainsOfInterestView。此外,如果您的最低部署目标允许,您可以使用 @StateObject 而不是 @ObservedObject。在这种情况下,viewModel 属性不会在每次视图更新时重置。

【讨论】:

    【解决方案2】:

    引用here的答案

    如果您的数据可以更改,则需要动态 ForEach 循环(带有显式 id 参数):

     ForEach(viewModel.domains.indices, id: \.self) { index in // <=
        // ...
     }
    

    在某些情况下,当您尝试修改 ForEach 循环中使用的数组时,Xcode 会警告您:

    ForEach(:content:) 应该只用于常量数据。而是使数据符合 Identifiable 或使用 ForEach(:id:content:) 并提供明确的 id!

    测试代码

    
    struct DomainsOfInterestView: View {
    
        @ObservedObject var viewModel: DomainsViewModel = DomainsViewModel()
        
        @State var isActive = true
        
        var body: some View {
                VStack(alignment: .center) {
                    HStack {
                        Text("Choose domains of interest for your profile")
                            .font(.headline)
                        Spacer()
                    }.padding(.bottom, 16)
                    
                    ForEach(viewModel.domains.indices, id: \.self) { index in // <=
                        Text(String(index))
                    }
    
                    Button(action: {
                    }) {
                        Text("Save")
                    }.padding(.top, 30)
                        .padding([.leading, .trailing], 30)
                    Spacer()
                }.padding([.leading, .trailing], 12)
                .navigationBarTitle("Domains of interest")
        }
    }
    
    
    class DomainsViewModel: ObservableObject {
    
        @Published var domains: [Int] = [Int]()
        
        init() {
            self.getDomains { (response) in
                print(response)
            }
        }
        
        func getDomains(completion: @escaping (Bool) -> Void) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.domains = [1,2,3,4,5]
                completion(true)
            }
        }
    
    }
    

    在 iOS 14 beta 6 上使用 Xcode 12 beta 6 测试

    【讨论】:

    • 我添加了,但无论如何,UI 没有更新。我使用了一些 mockDate,当在 viewModel 的 init() 中调用 getDomains() 时,“domains”的值正在发生变化。
    • 我添加了我测试过的代码,效果很好
    • 我想我找到了问题所在。看起来 getDomains() 函数执行了两次,我只使用这个视图构建了一个新项目,第一次时域更改为 3 个值的数组,第二次失败并在此之前使数组为空。这就是为什么在 UI 上看不到任何更改的原因,因为第二次调用该函数会使数组为空。但我不明白为什么它被调用两次,即使在其他视图中它也是一样的。看起来我做错了什么,但无法得到它。谢谢
    • 这取决于您放置DomainOfInterestView 的位置,因为每当初始化DomainOfInterestView 时都会初始化视图模型。您可以通过将DomainsViewModel 作为依赖项传递给DomainOfInterestView 来解决此问题。即DomainOfInterestView(viewModel: DomainsViewModel)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-06
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 2015-02-24
    • 2013-07-20
    相关资源
    最近更新 更多