Swiftui #EnvironmentObject update in View - swiftui

I am using an #EnvironmentObject (which is the ViewModel) and I have a demoData() function.
When I press it the data does change but my View is not updated.
How do I get the data to change in the View?
Thank you.
The view information:
import Combine
import SwiftUI
struct MainView: View {
#EnvironmentObject var entry:EntryViewModel
var body: some View {
TextField("Beg Value", text: self.$entry.data.beg)
TextField("Beg Value", text: self.$entry.data.end)
Button(action: { self.entry.demoData() }) { Text("Demo Data") }
}
}
ViewModel:
class EntryViewModel: ObservableObject {
#Published var data:EntryData = EntryData()
func demoData() {
var x = Int.random(in: 100000..<120000)
x = Int((Double(x)/100).rounded()*100)
data.beg = x.withCommas()
x = Int.random(in: 100000..<120000)
x = Int((Double(x)/100).rounded()*100)
data.end = x.withCommas()
}
Model:
EntryData:ObservableObject {
#Published var beg:String = ""
#Published var end:String = ""
}

This is because EntryData is a class and if you change its properties it will still be the same object.
This #Published will only fire when you reassign the data property:
#Published var data: EntryData = EntryData()
A possible solution is to use a simple struct instead of an ObservableObject class:
struct EntryData {
var beg: String = ""
var end: String = ""
}
When a struct is changed, it's copied and therefore #Published will send objectWillChange.

Related

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

How to use #FocusState with view models?

I'm using view models for my SwiftUI app and would like to have the focus state also in the view model as the form is quite complex.
This implementation using #FocusState in the view is working as expected, but not want I want:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
Button("Set Focus") {
hasFocus = true
}
}
}
}
class ViewModel: ObservableObject {
#Published var textField: String = ""
}
How can I put the #FocusState into the view model?
Assuming you have in ViewModel as well
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
...
}
you can use it like
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0 // << write !!
}
.onAppear {
self.hasFocus = viewModel.hasFocus // << read !!
}
}
}
as well as the same from Button if any needed.
I faced the same problem and ended up writing an extension that can be reused to sync both values. This way the focus can also be set from the view model side if needed.
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.sync($viewModel.hasFocus, with: _hasFocus)
}
}
extension View {
func sync<T: Equatable>(_ binding: Binding<T>, with focusState: FocusState<T>) -> some View {
self
.onChange(of: binding.wrappedValue) {
focusState.wrappedValue = $0
}
.onChange(of: focusState.wrappedValue) {
binding.wrappedValue = $0
}
}
}

update View when view model's, publish object's, property is updated

How to update view, when view models publish var's (user's) , name property is updated. I do know why its happening but what is the best way to update the view in this case.
class User {
var id = "123"
#Published var name = "jhon"
}
class ViewModel : ObservableObject {
#Published var user : User = User()
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += "update"
print( viewModel.user.name)
}
}
}
so one way i do it, is by using onReceive like this,
var body: some View {
userNameView
.onReceive(viewModel.user.$name){ output in
let tmp = viewModel.user
viewModel.user = tmp
print("onTapGesture",output)
}
}
but it is not a good approach it will update all view using users properties.
should i make a #state var for the name?
or should i just make a ObservedObject for user as well?
Make you class conform to ObservableObject
class User: ObservableObject {
var id = "123"
#Published var name = "jhon"
}
But he catch with that is that you have to observe it directly you can't chain it in a ViewModel
Use #ObservedObject var user: User in a View
You should use struct:
import SwiftUI
struct User {
var id: String
var name: String
}
class ViewModel : ObservableObject {
#Published var user : User = User(id: "123", name: "Mike")
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
userNameView
}
var userNameView: some View {
Text(viewModel.user.name)
.background(Color.red)
.onTapGesture {
viewModel.user.name += " update"
print( viewModel.user.name)
}
}
}

#Published property not updating view from nested view model - SwiftUI

I'm unsure why my view isn't getting updated when I have a view model nested inside another. My understanding was that a #Published property in the child view model would trigger a change in the parent viewModel, causing that to push changes to the UI.
This is the child view model:
class FilterViewModel : ObservableObject, Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
#Published var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
func changeSelected() {
self.selected = !self.selected
}
}
The following works as expected, on clicking the button the background colour is changed.
struct ContentView: View {
#ObservedObject var filterVM = FilterViewModel(name: "A")
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
However, I want to have an array of filter view models so tried:
class FilterListViewModel: ObservableObject {
#Published var filtersVMS = [FilterViewModel]()
init(){
filtersVMS = [
FilterViewModel(name: "A"),
FilterViewModel(name: "B"),
FilterViewModel(name: "C"),
FilterViewModel(name: "D")
]
}
}
However, the following view is not updated when clicking the button
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
Button(action: { filterListVM[0].changeSelected()}, label: {
Text(filterListVM[0].name)
.background(Color(filterListVM[0].backgroundColour))
})
}
Alternate solution is to use separated view for your sub-model:
struct FilterView: View {
#ObservedObject var filterVM: FilterViewModel
var body: some View {
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
}
so in parent view now we can just use it as
struct ContentView: View {
#ObservedObject var filterListVM = FilterListViewModel()
// ...
FilterView(filterVM: filterListVM[0])
}
The most simplest way is to define your FilterViewModel as struct. Hence, it is a value type. When a value changes, the struct changes. Then your ListViewModel triggers a change.
struct FilterViewModel : Identifiable {
var id = UUID().uuidString
var name = ""
var backgroundColour = ""
var selected = false
private var cancellables = Set<AnyCancellable>()
init(name: String){
self.name = name
$selected.map { _ in
self.selected ? "Orange" : "LightGray"
}
.assign(to: \.backgroundColour, on: self)
.store(in: &cancellables)
}
mutating func changeSelected() {
self.selected = !self.selected
}
}

How do I pass a subset of data to a view that has its own view model?

Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}