SwiftUI ViewModel не обновляет вычисляемые переменные - PullRequest
0 голосов
/ 29 октября 2019

У меня есть класс представления SwiftUI, который смог обновить свое собственное представление текста, так как текстовые поля с привязываемыми значениями были обновлены пользователем. Проблема заключалась в том, что все переменные содержались внутри самого класса 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

Ответы [ 2 ]

1 голос
/ 29 октября 2019

Вот полные коды, которые могут вас заинтересовать:

  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. Все переменные должны быть опубликованы, чтобы упростить процесс.

1 голос
/ 29 октября 2019

Измените вашу структуру KeViewModel на класс, удовлетворяющий протоколу ObservableObject. Кроме того, измените оболочки свойства @State на оболочки свойств @Published следующим образом:

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

Также пометьте свой экземпляр ViewModel оболочкой свойства @ObservedOjbect:

@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свойства объекта, не предоставляя его в качестве объекта среды вплоть до иерархии представления, но все равно получая обновления во всех необходимых местах автоматически.

...