Reputation: 1718
I have a SwiftUI view class that was able to update its own Text view as TextFields with bindable values were updated by the user. The problem was that all of the variables were contained within the View class itself. As soon as I extracted the variables to a view model class however the calculated fields no longer update as the bindable values are updated. Here's the (non-updating) code:
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))"
}
}
}
}
And here's the code that is able to update as the user inputs the values:
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()
}
}
This seems silly that I can't extract that variables to an external struct and would prefer to have a clean separation between my data and my view. Any assistance is appreciated. Finally, if you'd like to build and run the project yourself, it's available in it's entirety at https://github.com/jamesjmtaylor/wrd-ios
Upvotes: 1
Views: 2027
Reputation: 861
Change you KeViewModel struct to be class which satisfies to ObservableObject protocol. In addition change @State property wrappers to the @Published property wrappers just like this:
class KeViewModel: ObservableObject {
@Published var ap = ""
@Published var targetArmor = ""
@Published var targetRange = ""
@Published var weaponRange = ""
Also mark your ViewModel instance with @ObservedOjbect property wrapper:
@ObservedObject var vm: KeViewModel
Now you are injecting this view model to the specific view via constructor in TabView:
TabView {
KeView(vm: KeViewModel()).tabItem {
Text("KE")
Image("first")
...
and in the Content Preview:
struct KeView_Previews: PreviewProvider {
static var previews: some View {
KeView(vm: KeViewModel())
}
}
Now your View can observe ViewModel object for publishing new values of the ViewModel object properties without providing it as Environment Object down to the view hierarchy, but still getting updates in all necessary places automatically.
Upvotes: 1
Reputation: 11531
Here is the full codes you may interest :
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)
}
}
}
The model has to be a class
because it needs to conform observable
. All variables needs to be @published which make things easier.
Upvotes: 0