how to update SwiftUIView withOut #State variable? and segmentation fault issue - ios

I want increment a variable on Button action and show the value on Text. I can do it with #State var i: Int = 0 variable. But I want to update the value from another class or swift file. So that file cannot access this variable unless it is static. But State can't be static as it fails build with segmentation fault error while building.
So I thought of making it a simple variable. Then updating from that class and then assigning it with a #State variable. But that is not happening because to update variable I'm using another static function on SwiftUIView struct as well.
I can update the static variable but I can't update the View.
heres the code for UIVIew:
struct ContentView: View {
//private var Arr = [String]()
#State static var x: Int = 0 //Causes segmentation fault, Removing static runs but can't access var x
static var testVar: Int = 0
var body: some View {
VStack{
Text("Hello, Time: \(ContentView.testVar)")
Button(action: {
_ = timer().testMe() // calling other class method, is this Okay?
}) {
Text("Button")
}
}
}
static func bigGuy(){
testVar += 1
print("Hello You clicked this time: \(testVar)") // Without next line it can run and print with no issue.
self.x = testVar // Don't want to use this, But how do I update View ?
}
So how do I update this simple view?

Don't use static, instead put that variable into view model and use instance of view model in both views, so one modifies it another presents it, like in below example
class ViewModel: ObservableObject {
#Published var x: Int = 0
}
struct View1: View {
#ObservedObject var vm: ViewModel
var body: some View {
Text("\(mv.x)")
}
}
struct View2: View {
#ObservedObject var vm: ViewModel
var body: some View {
Button("Modify") { self.mv.x += 1 }
}
}
struct ContentView: View {
let vm = ViewModel()
var body: some View {
VStack {
View1(vm: self.vm)
View2(vm: self.vm)
}
}
}

Related

SwiftUI: Communication between different ViewModels

I have a parent view which contains two child views. Each child view gets passed a different EnvironmentObject from the parent view. As a representation for all kinds of different changes, the second child view contains a Button which can be used to call a function in its ViewModel which then is supposed to change a variable in the ViewModel of the first child view.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
var body: some View {
ZStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
...
}}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
var body: some View {
Button(action: {
viewModel_2.changeValue_in_ViewModel_1(value: 1)
}, label: {
Text("Tap to change value")
})
}}
class ViewModel_1: ObservableObject {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
}
}
Is there a way to make those two ViewModels able to communicate with each other?
Thanks!
It would be solved simply by ViewModel_2 referencing ViewModel_1.
However, it is not necessary to refer to all ViewModel_1, so you can separate only the desired logic using protocol and let ViewModel_2 own it.
This is the sample code for the above explanation.
Searching for dependency injection can yield a lot of information about it.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
init() {
let viewModel_1 = ViewModel_1()
_viewModel_1 = StateObject(wrappedValue: viewModel_1)
_viewModel_2 = StateObject(wrappedValue: ViewModel_2(changeValue: viewModel_1 as! ChangeValue))
}
var body: some View {
VStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}
}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
Text("\(viewModel_1.someValue)")
}
}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
#State var count: Int = 0
var body: some View {
Button(action: {
count = count + 1
viewModel_2.changeValue_in_ViewModel_1(value: count)
}, label: {
Text("Tap to change value")
})
}
}
protocol ChangeValue {
func changeValue(value: Int)
}
class ViewModel_1: ObservableObject, ChangeValue {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
private let changeValue: ChangeValue
init (changeValue: ChangeValue) {
self.changeValue = changeValue
}
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
changeValue.changeValue(value: value)
}
}
We don't actually use view model objects in SwiftUI. The View struct is a view model already, being a value type it's more efficient and less error prone than an object but the property wrappers make it behave like an object, SwiftUI diffs the View struct and it creates/updates actual UIView/NSViews on screen for us. If you use actual view model objects you'll get bugs and face the problems that you are experiencing.
You can group related #State vars into their own struct and use mutating func for logic. That way it can be tested independently but the best thing is any chance to a property of the struct is detected by SwiftUI as a change to the whole struct which makes its dependency tracking super fast.
environmentObject is designed to hold a store object that contains the model structs (usually in arrays) in #Published properties. There isn't usually more than one environmentObject. This object is usually responsible for persisting or syncing the model data.

Updating EnvironmentObject from within the View Model

In SwiftUI, I want to pass an environment object to a view model so I can change/update it. The EnvironmentObject is a simple AppState which consists of a single property counter.
class AppState: ObservableObject {
#Published var counter: Int = 0
}
The view model "CounterViewModel" updates the environment object as shown below:
class CounterViewModel: ObservableObject {
var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
The ContentView displays the value:
struct ContentView: View {
#ObservedObject var counterVM: CounterViewModel
init(counterVM: CounterViewModel) {
self.counterVM = counterVM
}
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
I am also injecting the state as shown below:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
let appState = AppState()
ContentView(counterVM: CounterViewModel(appState: appState))
.environmentObject(appState)
}
}
}
The problem is that when I click the increment button, the counterVM.counter never returns the updated value. What am I missing?
Your class CounterViewModel is an ObservableObject, but it has no #Published properties – so no changes will be published automatically to the views.
But you can manually publish changes by using objectWillChange.send():
func increment() {
objectWillChange.send()
appState.counter += 1
}
We actually don't use view model objects in SwiftUI for view data. We use an #State struct and if we need to mutate it in a subview we pass in a binding, e.g.
struct Counter {
var counter: Int = 0
mutating func increment() {
counter += 1
}
}
struct ContentView: View {
#State var counter = Counter()
var body: some View {
ContentView2(counter: $counter)
}
}
struct ContentView2: View {
#Binding var counter: Counter // if we don't need to mutate it then just use let and body will still be called when the value changes.
var body: some View {
VStack {
Text(counter.counter, format: .number) // the formatting must be done in body so that SwiftUI will update the label automatically if the region settings change.
Button("Increment") {
counter.increment()
}
}
}
}
I'm not sure why both the CounterViewModel and the AppState need to be observable objects, since you are using a view model to format the content of your models. I would consider AppState to be a model and I could therefore define it as a struct. The CounterViewModel will then be the ObservableObject and it published the AppState. In this way your code is clean and works.
Code for AppState:
import Foundation
struct AppState {
var counter: Int = 0
}
Code for CounterViewModel:
import SwiftUI
class CounterViewModel: ObservableObject {
#Published var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
Code for the ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var counterVM = CounterViewModel(appState: AppState())
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
Do remind, that in the View where you first define an ObservableObject, you define it with #StateObject. In all the views that will also use that object, you use #ObservedObject.
This code will work.
Did you check your xxxxApp.swift (used to be the AppDelegate) file ?
Sometimes Xcode would do it for you automatically, sometimes won't you have to add it manually and add your environment object. * It has to be the view that contains all the view you want to share the object to.
var body: some Scene {
WindowGroup {
VStack {
ContentView()
.environmentObject(YourViewModel())
}
}
}

Initializing #StateObject with parameters [duplicate]

This question already has answers here:
Initialize #StateObject with a parameter in SwiftUI
(12 answers)
Closed 1 year ago.
Suppose I have views like this:
struct Parent: View {
var data: Int
var body: some View {
Child(state: ChildState(data: data))
}
}
struct Child: View {
#StateObject var state: ChildState
var body: {
...
}
}
class ChildState: ObservableObject {
#Published var property: Int
...
init(data: Int) {
// ... heavy lifting
}
}
My understanding is that the init method for Child should not do any heavy computation, since it could be called over and over, by SwiftUI. So heavy lifting should be done in the ChildState class, which is marked with the #StateObject decorator, to ensure it persists through render cycles. The question is, if Parent.data changes, how do I propagate that down so that ChildState knows to update as well?
That is to say, I DO want a new instance of ChildState, or an update to ChildState if and only if Parent.data changes. Otherwise, ChildState should not change.
You need to make the parent state observable too. I'd do it like this:
class ParentState: ObservableObject {
#Published var parentData = 0
}
class ChildState: ObservableObject {
#Published var childData = 0
}
struct Parent: View {
#StateObject var parentState = ParentState()
#StateObject var childState = ChildState()
var body: some View {
ZStack {
Color.black
VStack {
Text("Parent state \(parentState.parentData) -- click me")
.onTapGesture {
parentState.parentData += 1
}
Child(parentState: parentState, childState: childState)
}
}
}
}
struct Child: View {
#ObservedObject var parentState: ParentState
#ObservedObject var childState: ChildState
var body: some View{
Text("Child state \(childState.childData) parentState \(parentState.parentData) -- click me")
.onTapGesture {
childState.childData += 1
}
}
}
struct ContentView: View {
var body: some View {
Parent()
}
}
Tested with Xcode 13.1
Of course, there's nothing wrong with passing the child state object to the initializer of the child view, the way you've shown it. It will work fine still, I just wanted to make the example as clear as possible.

Swift static value stuck at original value

Can someone tell me why my static variable isn't changing when I use a += on it? I have been stuck on this bug for a while.
#State static var counter:Int = 2
#State static var increment:Int = 1
var body: some View {
NavigationView {
VStack {
Text("Value: " + String(ContentView.counter))
Button(action: {
ContentView.counter += ContentView.increment
print(ContentView.counter)
If you want to share state between different instances of your view, you should inject said state as a Binding rather than declaring an #State static property inside the View.
#State properties should only be updated from inside the (body of the) view itself, which isn't the case if you declare an #State property as static. To ensure the update can only happen from inside the View, all #State properties must be private (or at the very least private(set)).
Not sure how exactly you create different instances of your view, but here's an example of how you could introduce shared state between them in a correct way:
struct Cell: View {
#Binding private var counter: Int
#Binding private var increment: Int
init(counter: Binding<Int>, increment: Binding<Int>) {
self._counter = counter
self._increment = increment
}
var body: some View {
VStack {
Text("Value: \(counter)")
Button("Increment by \(increment)", action: { counter += increment })
}
}
}
struct Counter: View {
#State private var counter: Int = 2
#State private var increment: Int = 1
var body: some View {
VStack {
Cell(counter: $counter, increment: $increment)
Cell(counter: $counter, increment: $increment)
Cell(counter: $counter, increment: $increment)
}
}
}

SwiftUI MVVM: child view model re-initialized when parent view updated

I'm attempting to use MVVM in a SwiftUI app, however it appears that view models for child views (e.g. ones in a NavigationLink) are re-initialized whenever an ObservableObject that's observed by both the parent and child is updated. This causes the child's local state to be reset, network data to be reloaded, etc.
I'm guessing it's because this causes parent's body to be reevaluated, which contains a constructor to SubView's view model, but I haven't been able to find an alternative that lets me create view models that don't live beyond the life of the view. I need to be able to pass data to the child view model from the parent.
Here's a very simplified playground of what we're trying to accomplish, where incrementing EnvCounter.counter resets SubView.counter.
import SwiftUI
import PlaygroundSupport
class EnvCounter: ObservableObject {
#Published var counter = 0
}
struct ContentView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: .init())
}
.environmentObject(envCounter)
}
}
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Sub view")
Button(action: { self.viewModel.counter += 1 }) {
Text("SubView counter is at \(self.viewModel.counter)")
}
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
A new property wrapper is added to SwiftUI in Xcode 12, #StateObject. You should be able to fix it by simply changing #ObservedObject for #StateObject as follows.
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#StateObject var viewModel: ViewModel // change on this line
var body: some View {
// ...
}
}
To solve this problem I created a custom helper class called ViewModelProvider.
The provider takes a hash for your view, and a method that builds the ViewModel. It then either returns the ViewModel, or builds it if its the first time that it received that hash.
As long as you make sure the hash stays the same as long as you want the same ViewModel, this solves the problem.
class ViewModelProvider {
private static var viewModelStore = [String:Any]()
static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM {
if let vm = viewModelStore[hash] as? VM {
return vm
} else {
let vm = builder()
viewModelStore[hash] = vm
return vm
}
}
}
Then in your View, you can use the ViewModel:
Struct MyView: View {
#ObservedObject var viewModel: MyViewModel
public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) {
self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) {
MOFOnboardingFlowViewModel(
pages: pages,
baseStyleConfig: style,
buttonConfig: buttonConfig,
onFinish: onFinish
)
}
}
}
In this example, there are two parameters. Only thisParameterChangesVM is used in the hash. This means that even if thisParameterDoesntChangeVM changes and the View is rebuilt, the view model stays the same.
I was having the same problem, your guesses are right, SwiftUI computes all your parent body every time its state changes. The solution is moving the child ViewModel init to the parent's ViewModel, this is the code from your example:
class EnvCounter: ObservableObject {
#Published var counter = 0
#Published var subViewViewModel = SubView.ViewModel.init()
}
struct CounterView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: envCounter.subViewViewModel)
}
.environmentObject(envCounter)
}
}

Resources