【问题标题】:SwiftUI ViewModel doesn't update computed variablesSwiftUI ViewModel 不更新计算变量
【发布时间】:2020-02-24 17:09:38
【问题描述】:

我有一个 SwiftUI 视图类,它能够更新自己的 Text 视图,因为用户更新了具有可绑定值的 TextField。问题是所有变量都包含在 View 类本身中。一旦我将变量提取到视图模型类,计算字段就不再随着可绑定值的更新而更新。这是(非更新)代码:

struct KeView: View {
    var vm = KeViewModel()

    var body: some View {
        return VStack {
            Image("ke")
            InputFieldView(category: Localizable.weaponAp(), input: vm.$ap)
            InputFieldView(category: Localizable.targetArmor(), input: vm.$targetArmor)
            InputFieldView(category: Localizable.weaponRange(), input: vm.$weaponRange)
            InputFieldView(category: Localizable.targetRange(), input: vm.$targetRange)
            Text(vm.damageString)
            .foregroundColor(Color.white)
            .padding()
                .background(vm.damageColor)
            .frame(maxHeight: .infinity)
        }
    }
}


struct KeView_Previews: PreviewProvider {
    static var previews: some View {
        KeView()
    }
}

struct KeViewModel {
    @State var ap = ""
    @State var targetArmor = ""
    @State var targetRange = ""
    @State var weaponRange = ""

    var damageColor: Color {
        if damageString.contains(Localizable.outOfRange()) { return Color.red }
        if damageString.contains(Localizable.inefficient()) { return Color.black }
        let d = damageString.split(separator: " ").last ?? ""
        if (Double(d) ?? 0) < 10 { return Color.blue }
        return Color.red
    }

    var damageString : String {
            guard let ap = Double(ap),
                let weaponRange = Double(weaponRange),
                let targetRange = Double(targetRange),
                let targetArmor = Double(targetArmor) else {
                    return Localizable.damagePrefix() + " 0"
        }
            if (weaponRange < targetRange){
                return Localizable.outOfRange()
            } else {
                let difference = (weaponRange - targetRange) / 175
                //print("Difference is equal to",difference)
                let actualAp = ap + difference
                //print("actual AP is equal to",actualAp)
                if (actualAp < targetArmor){
                    return Localizable.inefficient()
                } else if (targetArmor == 0){
                    return Localizable.damagePrefix()
                        + "\(round(actualAp * 2))"
                } else {
                    return Localizable.damagePrefix()
                        + " \(round((actualAp - Double(targetArmor)) / 2 + 1.0))"
                }
            }
    }
}

这是能够在用户输入值时更新的代码:

struct KeView: View {
    @State var ap = ""
    @State var targetArmor = ""
    @State var targetRange = ""
    @State var weaponRange = ""

    var damageColor: Color {
        if damageString.contains(Localizable.outOfRange()) { return Color.red }
        if damageString.contains(Localizable.inefficient()) { return Color.black }
        let d = damageString.split(separator: " ").last ?? ""
        if (Double(d) ?? 0) < 10 { return Color.blue }
        return Color.red
    }

    var damageString : String {
            guard let ap = Double(ap),
                let weaponRange = Double(weaponRange),
                let targetRange = Double(targetRange),
                let targetArmor = Double(targetArmor) else {
                    return Localizable.damagePrefix() + " 0"
        }
            if (weaponRange < targetRange){
                return Localizable.outOfRange()
            } else {
                let difference = (weaponRange - targetRange) / 175
                //print("Difference is equal to",difference)
                let actualAp = ap + difference
                //print("actual AP is equal to",actualAp)
                if (actualAp < targetArmor){
                    return Localizable.inefficient()
                } else if (targetArmor == 0){
                    return Localizable.damagePrefix()
                        + "\(round(actualAp * 2))"
                } else {
                    return Localizable.damagePrefix()
                        + " \(round((actualAp - Double(targetArmor)) / 2 + 1.0))"
                }
            }
    }

    var body: some View {
        return VStack {
            Image("ke")
            InputFieldView(category: Localizable.weaponAp(), input: $ap)
            InputFieldView(category: Localizable.targetArmor(), input: $targetArmor)
            InputFieldView(category: Localizable.weaponRange(), input: $weaponRange)
            InputFieldView(category: Localizable.targetRange(), input: $targetRange)
            Text(String(self.damageString))
            .foregroundColor(Color.white)
            .padding()
            .background(damageColor)
            .frame(maxHeight: .infinity)
        }
    }
}


struct KeView_Previews: PreviewProvider {
    static var previews: some View {
        KeView()
    }
}

这似乎很愚蠢,因为我无法将这些变量提取到外部结构中,并且更愿意在我的数据和我的视图之间有一个清晰的分离。任何帮助表示赞赏。最后,如果您想自己构建和运行该项目,可以在 https://github.com/jamesjmtaylor/wrd-ios

【问题讨论】:

    标签: swift mvvm viewmodel swiftui xcode11


    【解决方案1】:

    这是您可能感兴趣的完整代码:

      window.rootViewController = UIHostingController(rootView: KeView().environmentObject(KeViewModel())
    
    class KeViewModel : ObservableObject {
    @Published var ap = ""
    @Published var targetArmor = ""
    @Published var targetRange = ""
    @Published var weaponRange = ""
    
    var damageColor: Color {
        if damageString.contains(Localizable.outOfRange()) { return Color.red }
        if damageString.contains(Localizable.inefficient()) { return Color.black }
        let d = damageString.split(separator: " ").last ?? ""
        if (Double(d) ?? 0) < 10 { return Color.blue }
        return Color.red
    }
    
    var damageString : String {
            guard let ap = Double(ap),
                let weaponRange = Double(weaponRange),
                let targetRange = Double(targetRange),
                let targetArmor = Double(targetArmor) else {
                    return Localizable.damagePrefix() + " 0"
        }
            if (weaponRange < targetRange){
                return Localizable.outOfRange()
            } else {
                let difference = (weaponRange - targetRange) / 175
                //print("Difference is equal to",difference)
                let actualAp = ap + difference
                //print("actual AP is equal to",actualAp)
                if (actualAp < targetArmor){
                    return Localizable.inefficient()
                } else if (targetArmor == 0){
                    return Localizable.damagePrefix()
                        + "\(round(actualAp * 2))"
                } else {
                    return Localizable.damagePrefix()
                        + " \(round((actualAp - Double(targetArmor)) / 2 + 1.0))"
                }
            }
    }
    }
    
    
    struct KeView: View {
    @EnvironmentObject var model: KeViewModel
    var body: some View {
        return VStack {
            Image("ke")
            InputFieldView(category: Localizable.weaponAp(), input: $model.ap)
            InputFieldView(category: Localizable.targetArmor(), input: $model.targetArmor)
            InputFieldView(category: Localizable.weaponRange(), input: $model.weaponRange)
            InputFieldView(category: Localizable.targetRange(), input: $model.targetRange)
            Text(String(model.damageString))
            .foregroundColor(Color.white)
            .padding()
            .background(model.damageColor)
            .frame(maxHeight: .infinity)
        }
    }
    
    }
    

    模型必须是class,因为它需要符合observable。所有变量都需要@published,这使事情变得更容易。

    【讨论】:

    • 做到了。因为 KeView 托管在 tabItem 中,所以我必须对我的 rootViewController 进行以下更改:TabView {KeView().environmentObject(KeViewModel()).tabItem.tag(0)}
    【解决方案2】:

    将您的 KeViewModel 结构更改为满足 ObservableObject 协议的类。另外将@State 属性包装器更改为@Published 属性包装器,如下所示:

    class KeViewModel: ObservableObject {
        @Published var ap = ""
        @Published var targetArmor = ""
        @Published var targetRange = ""
        @Published var weaponRange = ""
    

    同时使用 @ObservedOjbect 属性包装器标记您的 ViewModel 实例:

    @ObservedObject var vm: KeViewModel
    

    现在您正在通过 TabView 中的构造函数将此视图模型注入特定视图:

    TabView {
                    KeView(vm: KeViewModel()).tabItem {
                        Text("KE")
                        Image("first")
    ...
    

    在内容预览中:

    struct KeView_Previews: PreviewProvider {
        static var previews: some View {
            KeView(vm: KeViewModel())
        }
    }
    

    现在您的 View 可以观察 ViewModel 对象以发布 ViewModel 对象属性的新值,而无需将其作为 Environment Object 向下提供给视图层次结构,但仍会自动在所有必要的地方获取更新。

    【讨论】:

    • 这会引发 Thread 1: Fatal error: No ObservableObject of type KeViewModel found 的运行时异常。通过按照批准的答案中的建议添加TabView {KeView().environmentObject(KeViewModel()).tabItem.tag(0)},我能够绕过它。
    • 我的错!为了让事情最终正常工作,需要将 KeViewModel 实例声明为 ObservedObject,再次使用属性包装器@ObservedObject。与环境对象相比,这种方式是有益的,因为后者适用于所有视图层次结构(如果它被引入)。这不是内存效率。我已更新我的答案以反映所需的更改。
    • 做到了。感谢您提供替代解决方案!
    猜你喜欢
    • 1970-01-01
    • 2021-08-06
    • 2020-05-14
    • 2021-09-07
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 2021-12-19
    相关资源
    最近更新 更多