So I have read a lot about swiftUI and I am confused. I understand the #State and how it update the view behind the scenes. The thing I can't understand is why when I the subview in this example is automatically updated when the state changes in the top view/struct given the fact that on the subview the var subname is not a #State property. I would expect this not to be updated. Can someone enlighten me please?
import SwiftUI
struct SwiftUIView: View {
#State private var name = "George"
var body: some View {
VStack{
SubView(subName: name).foregroundColor(.red)
Button(action: {
self.name = "George2"
}) {
Text("Change view name")
}
}
}
}
struct SubView: View {
var subName:String
var body: some View {
Text(subName)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
PS.
If I change the subview to
struct SubView: View {
#State var subName:String
var body: some View {
Text(subName)
}
}
it's not updated when I press the button.
var subName: String
struct SubView: View {
var subName: String
// ...
}
In the above example you don't really have a state. When the subName property in the parent view changes, the whole SubView is redrawn and thus provided with a new value for the subName property. It's basically a constant (try changing it to let, you'll have the same result).
#State var subName: String
struct SubView: View {
#State var subName: String
// ...
}
Here you do have a #State property which is initialised once (the first time the SubView is created) and remains the same even when a view is redrawn. Whenever you change the subName variable in the parent view, in the SubView it will stay the same (it may be modified from within the SubView but will not change the parent's variable).
#Binding var subName: String
struct SubView: View {
#Binding var subName: String
// ...
}
If you have a #Binding property you can directly access the parent's #State variable. Whenever it's changed in the parent it's changed in the SubView (and the other way around) - that's why it's called binding.
Whenever an #State property of a View changes, the view is refreshed, which means its whole body is recalculate. Since SubView is part of the body of SwiftUIView, whenever SwiftUIView refreshes itself, it redraws its SubView using the new value of its #State name property.
Related
I'm trying to optimize my SwiftUI app. I have a strange behavior with a ViewModel stored as a #StateObject in its View. To understand the issue, I made a small project that reproduces it.
ContentView contains a button to open ChildView in a sheet. ChildView is stored as property as I don't want to recreate it every time the sheet is open by user (this works):
struct ContentView: View {
#State private var displayingChildView = false
private let childView = ChildView()
var body: some View {
Button(action: {
displayingChildView.toggle()
}, label: {
Text("Display child view")
})
.sheet(isPresented: $displayingChildView, content: {
childView // instead of: ChildView()
})
}
}
ChildView code:
struct ChildView: View {
#StateObject private var viewModel = ViewModel()
init() {
print("init() of ChildView")
}
var body: some View {
VStack {
Button(action: {
viewModel.add()
}, label: {
Text("Add 1 to count")
})
Text("Count: \(viewModel.count)")
}
}
}
And its ViewModel:
class ViewModel: ObservableObject {
#Published private(set) var count = 0
init() {
print("init() of ViewModel")
}
func add() {
count += 1
}
}
Here is the issue:
The ViewModel's init is called every time user opens the sheet. Why?
As ViewModel is a #StateObject in ChildView and ChildView is only init once, I am expecting that ViewModel is also only init once.
I have read this article that says that :
Observed objects marked with the #StateObject property wrapper don’t get destroyed and re-instantiated at times their containing view struct redraws.
Or here:
Use #StateObject once for each observable object you use, in whichever part of your code is responsible for creating it.
So I understand that ViewModel should stay alive, especially as ChildView is not destroyed.
And what confuses me the most is that if I replace #StateObject with #ObservedObject it works as expected. But it is not recommended to store an #ObservedObject inside a View.
Can anyone explain why this behavior and how to fix it as expected (ViewModel init should be called once) ?
A possible solution:
I've found a possible solution to fix this behavior:
a. Move the declaration of ViewModel into ContentView:
#StateObject private var viewModel = ViewModel()
b. Change the declaration of ViewModel in ChildView to be an EnvironmentObject:
#EnvironmentObject private var viewModel: ViewModel
c. And inject it in childView:
childView
.environmentObject(viewModel)
That means it's ContentView that is responsible to keep the ChildView's ViewModel alive. It works, but I find this solution quite ugly:
All future child Views of ChildView could get access to ViewModel through environment objects. But it's no sense as it should be only useful for its View.
I would prefer declare a ViewModel inside its View instead of inside its parent View.
And this solution still doesn't explain above questions about #StateObject that should stay alive...
SwiftUI initializes the #State variables when a view is inserted into the view hierarchy. This is why your attempt to keep the state of the child view alive by assigning it to a var fails. Every time your sheet is presented, the child view is added to the view hierarchy and its state variables are initialized.
The correct way to do this is to pass the viewModel to the child view.
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
#State private var displayingChildView = false
var body: some View {
Button(action: {
displayingChildView.toggle()
}, label: {
Text("Display child view")
})
.sheet(isPresented: $displayingChildView, content: {
ChildView(viewModel: viewModel)
})
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Button(action: {
viewModel.add()
}, label: {
Text("Add 1 to count")
})
Text("Count: \(viewModel.count)")
}
}
}
Objects slow down SwiftUI, to use it effectively we need to forget about view model objects and learn value types, structs, mutating func, closure captures, etc. Here is how it should be done:
struct Counter {
private(set) var count = 0
init() {
print("init() of Counter")
}
mutating func add() {
count += 1
}
}
struct ChildView: View {
#State private var counter = Counter()
init() {
print("init() of ChildView")
}
var body: some View {
VStack {
Button(action: {
counter.add()
}, label: {
Text("Add 1 to count")
})
Text("Count: \(counter.count)")
}
}
}
When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.
I'm trying to re create an older version of my Onboarding setup with the new SwiftUI and when I try to share the state so the view changes, it simply doesn't know that something has changed, this is what I'm doing:
In the main .swift struct (not ContentView.swift) I defined the pages like this:
#main
struct AnotherAPP: App {
#ObservedObject var onBoardingUserDefaults = OnBoardingUserDefaults()
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
// Onboarding screen
if (onBoardingUserDefaults.isOnBoardingDone == false) {
OnboardingPageView()
} else {
UserLoginView()
}
}
}
}
So on the onBoarding page when I click the button to go to the login, it stores it, but it doesn't actually refreshes the view. There (in the OnboardingPageView.swift) I call the UserDefaults like this:
#ObservedObject private var onBoardingUserDefaults = OnBoardingUserDefaults()
and on the button I change it like this:
self.onBoardingUserDefaults.isOnBoardingDone = true
UserDefaults.standard.synchronize()
So what's going on?
I know for instance if I create a #State on the #main and I bind it to the OnboardingPageView it works, as soon as I hit that button it takes me there.
You can use AppStorage to manage UserDefaults variable in multiple views:
#main
struct TestApp: App {
#AppStorage("isOnBoardingDone") var isOnBoardingDone = false
var body: some Scene {
WindowGroup {
if !isOnBoardingDone {
OnboardingPageView(isOnBoardingDone: $isOnBoardingDone)
} else {
UserLoginView()
}
}
}
}
struct OnboardingPageView: View {
#Binding var isOnBoardingDone: Bool
var body: some View {
Button("Complete") {
isOnBoardingDone = true
}
}
}
If I understood correctly, you are trying to pass the value of a state variable in the Content View to another view in the same app. For simplicity, Let's say your variable is initialised as follows in ContentView:
#State private var countryIndex = 0 //Assuming the name of the variable is countryIndex
Now, to transfer the value write the following in the Content View (or wherever the variable is initially):
//Other code
NavigationLink(destination: NextPage(valueFromContentView: $countryIndex)) {
Text("Moving On")
}//In this case, the variable that will store the value of countryIndex in the other view is called valueFromContentView
//Close your VStacks and your body and content view with a '}'
In your second view or the other view, initialise a Binding variable called valueFromContentView using:
#Binding var valueFromContentView: Int
Then, scroll down to the code that creates your previews. FYI, It is another struct called ViewName_Previews: PreviewProvider { ... }
IF you haven't changed anything, it will be:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
}
}
Remember, my second view is called NextPage.
Inside the previews braces, enter the code:
NextPage(valueFromContentView: .constant(0))
So, the code that creates the preview for your application now looks like:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
NextPage(valueFromContentView: .constant(0)) //This is what you add
}
}
Remember, NextPage is the name of my view and valueFromContentView is teh binding variable that I initialised above
Like this, you now can transfer the value of a variable in one view to another view.
Problem
In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.
A normal ViewModel looks a bit like this:
class SomeViewModel: ObservableObject {
#Published var state = 1
// Logic and calls of Business Logic goes here
}
and is used like so:
struct SomeView: View {
#ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).
In my opinion, the ViewModel should stay, or the State should persist.
If I replace the ViewModel with a #State Property and use the int (in this example) directly it stays persisted and does not get recreated:
struct SomeView: View {
#State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
This does obviously not work for more complex States. And if I set a class for #State (like the ViewModel) more and more Things are not working as expected.
Question
Is there a way of not recreating the ViewModel every time?
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
Why is #State keeping the State over the redraw?
I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.
Duplicate Question
I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.
Edit (adding more detailed Example)
When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the NavigationLink, which would then lead to a recreation of the ViewModel.
I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Finally, there is a Solution provided by Apple: #StateObject.
By replacing #ObservedObject with #StateObject everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
#State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
#Published var title = 0
}
struct TestView2: View {
#StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.
struct MyView: View {
#State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
#ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.
Is there a way of not recreating the ViewModel every time?
Yes, keep ViewModel instance outside of SomeView and inject via constructor
struct SomeView: View {
#ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
No needs. #ObservedObject is-a already DynamicProperty similarly to #State
Why is #State keeping the State over the redraw?
Because it keeps its storage, ie. wrapped value, outside of view. (so, see first above again)
You need to provide custom PassThroughSubject in your ObservableObject class. Look at this code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//#ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
First, I using TextChanger to pass new value of .text to .onReceive(...) in CustomState View. Note, that onReceive in this case gets PassthroughSubject, not the ObservableObjectPublisher. In last case you will have only Publisher.Output in perform: closure, not the NewValue. state.text in that case would have old value.
Second, look at the ComplexState class. I made an objectWillChange property to make text changes send notification to subscribers manually. Its almost the same like #Published wrapper do. But, when the text changing it will send both, and objectWillChange.send() and textChanged.send(newValue). This makes you be able to choose in exact View, how to react on state changing. If you want ordinary behavior, just put the state into #ObservedObject wrapper in CustomStateContainer View. Then, you will have all the views recreated and this section will get updated values too:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove #ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.
update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// #Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
#ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
#State private var text: String = ""
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.
Is that what you needed?
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
.environmentObject(viewModel)
Just init viewModel somewhere it will not be reset(example root view).
I have a modal built with SwitftUI which has a TextField with onCommit: code which saves user input from #State variable to file when user taps "return" on keyboard.
However, if user types something inside TextField and then dismisses the modal without pressing "return", the onCommit: code doesn't fire and user input stays unsaved. How do I fire some code accessing inner variable of my modal View when it is dismissed?
Try the following:
Instead having a private #State var on your modal, make it an internal #Binding that you pass into the modal from the call site. This way the modified bound variable is available on both the caller and the modal view.
import SwiftUI
struct ContentView: View {
#State var dismiss = false
#State var txt = ""
#State var store = ""
var body: some View {
VStack {
Text("modal").sheet(isPresented: $dismiss, onDismiss: {
self.store = self.txt
}) {
TextField("txt", text: self.$txt) {
self.store = self.txt
}.padding().border(Color.red)
}.onTapGesture {
self.dismiss.toggle()
}
Text(store)
}
}
}
struct ContetView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}