SwiftUI ViewModel не обновляет вычисляемые переменные

У меня есть класс представления 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


person James Jordan Taylor    schedule 29.10.2019    source источник


Ответы (2)


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

  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, чтобы упростить задачу.

person E.Coms    schedule 29.10.2019
comment
Это сработало. Поскольку KeView размещался в tabItem, мне пришлось внести следующие изменения в свой rootViewController: TabView {KeView().environmentObject(KeViewModel()).tabItem.tag(0)} - person James Jordan Taylor; 29.10.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())
    }
}

Теперь ваше представление может наблюдать за объектом ViewModel для публикации новых значений свойств объекта ViewModel, не предоставляя его как объект среды до иерархии представлений, но при этом автоматически получая обновления во всех необходимых местах.

person SerhiiK    schedule 29.10.2019
comment
Это вызывает исключение времени выполнения Thread 1: Fatal error: No ObservableObject of type KeViewModel found. Добавив TabView {KeView().environmentObject(KeViewModel()).tabItem.tag(0)}, как было предложено в утвержденном ответе, я смог обойти это. - person James Jordan Taylor; 29.10.2019
comment
Моя вина! Чтобы все наконец заработало, необходимо объявить экземпляр KeViewModel как ObservedObject, снова используя оболочку свойств @ObservedObject. Этот способ выгоден по сравнению с Environment Object, потому что последний доступен для всей иерархии View вниз, где он был введен. Это неэффективно с памятью. Я обновил свой ответ, чтобы отразить необходимые изменения. - person SerhiiK; 01.11.2019
comment
Это сработало. Спасибо за альтернативное решение! - person James Jordan Taylor; 03.11.2019