Why does my SwiftUI View not update on updating of an #State var? - swift

I am having a strange issue with an #State var not updating an iOS SwiftUI view.
I have an edit screen for themes for a small game with a NavigationView with a list of game themes. When in edit mode and I select one of these themes, I open up an editor view, passing the theme as a binding to the editor view struct.
In my editor view I then have sections that allow the user to edit properties of the theme. I do not want to use bindings to the various theme properties in my edit fields because I do not want the changes to take effect immediately. Instead, I have created #State vars for each of these properties and then use bindings to these in the edit fields. That way, I give the user the option to either cancel without and changes taking effect, or select "Done" to assign the changes back to the theme via the binding.
In order to initialise the #State vars I have an onAppear block that assign the #State vars values from the respective theme properties.
The issue I am having is that when the onAppear block is executed and the vars are assigned, the relevant edit fields are not updating!
Here is a cut-down version of my code:
struct EditorView: View {
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#Binding var theme: GameTheme
#State private var name = ""
...
var body: some View {
NavigationView {
Form {
nameSection
...
}
.navigationTitle("Edit \(theme.name)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancel)
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: saveTheme)
.disabled(!canSaveTheme)
}
}
.onAppear {
name = theme.name
...
}
}
.frame(minWidth: Constants.minViewSize.width, minHeight: Constants.minViewSize.height)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
...
}
So the view gets shown an on appearing, the #State var name does correctly get assigned the value from theme.name; however, this allocation does not cause an update of the view and the value of "name" is not entered into the TextField.
Interestingly, and I do not know if this is a good thing to do, if I wrap the contents of the onAppear block in a DispatchQueue.main.async, everything works fine!
i.e.
.onAppear {
DispatchQueue.main.async {
name = theme.name
...
}
}
Does anyone have any idea as to how, within the onAppear, I can force a view refresh? Or, why the assignment to "name" does not force an update?
Thanks.

This isn't the answer per se, but I went ahead and created a new iOS project with the following code (based on your post, but I cleaned it up a bit and came up with the missing GameTheme object myself).
It's more or less the same, and shows that your posted structure does re-render.
I'm wondering if there's more to the code we can't see in your post that could be causing this.
Are you possibly setting the name state variable anywhere else in a way that could be overriding the value on load?
import SwiftUI
#main
struct TestIOSApp: App {
#State var gameTheme: GameTheme = GameTheme(name: "A game theme")
var body: some Scene {
WindowGroup {
ContentView(theme: $gameTheme)
}
}
}
struct GameTheme {
var name:String;
}
struct ContentView: View {
#Binding var theme:GameTheme;
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#State private var name = "DEFAULT SHOULD NOT BE DISPLAYED"
var body: some View {
NavigationView {
Form {
nameSection
}
.navigationTitle("Edit \(theme.name)")
.onAppear {
name = theme.name
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: {})
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: {})
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
}

I seem to have solved my problem with an init(). I created init(theme: Binding<GameTheme>) and then within the init assigned the theme via _theme = theme and then assigned the name via _name = State(initialValue: theme.name.wrappedValue).

Related

SwiftUI detect edit mode

I've returned to iOS development after a while and I'm rebuilding my Objective-C app from scratch in SwiftUI.
One of the things I want to do is use the default Edit Mode to allow entries in a List (backed by Core Data on CloudKit) to switch between a NavigationLink to a detail view and an edit view.
The main approach seems to be to handle it through a if statement that detects edit mode. The Apple documentation provides the following snippet for this approach on this developer page: https://developer.apple.com/documentation/swiftui/editmode
#Environment(\.editMode) private var editMode
#State private var name = "Maria Ruiz"
var body: some View {
Form {
if editMode?.wrappedValue.isEditing == true {
TextField("Name", text: $name)
} else {
Text(name)
}
}
.animation(nil, value: editMode?.wrappedValue)
.toolbar { // Assumes embedding this view in a NavigationView.
EditButton()
}
}
However, this does not work (I've embedded the snippet in a NavigationView as assumed).
Is this a bug in Xcode 13.4.1? iOS 15.5? Or am I doing something wrong?
Update1:
Based on Asperi's answer I came up with the following generic view to handle my situation:
import SwiftUI
struct EditableRow: View {
#if os(iOS)
#Environment(\.editMode) private var editMode
#endif
#State var rowView: AnyView
#State var detailView: AnyView
#State var editView: AnyView
var body: some View {
NavigationLink{
if(editMode?.wrappedValue.isEditing == true){
editView
}
else{
detailView
}
}label: {
rowView
}
}
}
struct EditableRow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
VStack {
EditButton()
EditableRow(rowView: AnyView(Text("Row")), detailView: AnyView(Text("Detail")), editView: AnyView(Text("Edit")))
}
}
}
The preview works as expected, but this works partially in my real app. When I implement this the NavigationLink works when not in Edit Mode, but doesn't do anything when in Edit Mode. I also tried putting the whole NavigationLink in the if statement but that had the same result.
Any idea why this isn't working?
Update2:
Something happens when it's inside a List. When I change the preview to this is shows the behavior I'm getting:
struct EditableRow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
List {
EditableRow(rowView: AnyView(GroupRow(title: "Title", subTitle: "Subtitle", type: GroupType.personal)), detailView: AnyView(EntryList()), editView: AnyView(Text("Edit")))
}
.navigationBarItems(trailing:
HStack{
#if os(iOS)
EditButton()
#endif
}
)
}
}
}
Update3:
Found this answer: SwiftUI - EditMode and PresentationMode Environment
This claims the default EditButton is broken, which seems to be true. Replacing the default button with a custom one works (be sure to add a withAnimation{} block to get all the behavior from the stock button.
But it still doesn't work for my NavigationLink...
Update4:
Ok, tried passing an "isEditing" Bool to the above View, not to depend on the Environment variable being available. This works as long as the View (a ForEach within a List in my case) isn't in "Editing Mode" whatever happens at that point breaks any NavigationLink it seems.
Update5:
Basically my conclusion is that the default Edit Mode is meant to edit the "List Object" as a whole enabling moving and deleting of rows. In this mode Apple feels that editing the rows themselves isn't something you'd want to do. I can see this perspective.
If, however, you still want to enable a NavigationLink from a row in Edit Mode, this answer should help:
How to make SwiftUI NavigationLink work in edit mode?
Asperi's answer does cover why the detection doesn't work. I did find that Edit Mode detection does work better when setting the edit mode manually and not using the default EditButton, see the answer above for details.
It is on same level so environment is not visible, because it is activated for sub-views.
A possible solution is to separate dependent part into standalone view, like
Form {
InternalView()
}
.toolbar {
EditButton()
}
Tested with Xcode 13.4 / iOS 15.5
Test module on GitHub
#Asperi's answer worked well for me. However I wanted to still be able to access the editMode in the same hierarchy. As a workaround I created the following:
Usage
struct ContentView: View {
#State
private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
Form {
if editMode.isEditing == true {
Color.red
} else {
Color.blue
}
}
.editModeFix($editMode)
.toolbar {
EditButton()
}
}
}
}
Implementation
extension View {
func editModeFix(_ editMode: Binding<EditMode>) -> some View {
modifier(EditModeFixViewModifier(editMode: editMode))
}
}
private struct EditModeFixView: View {
#Environment(\.editMode)
private var editModeEnvironment
#Binding
var editMode: EditMode
var body: some View {
Color.clear
.onChange(of: editModeEnvironment?.wrappedValue) { editModeEnvironment in
if let editModeEnvironment = editModeEnvironment {
editMode = editModeEnvironment
}
}
.onChange(of: editMode) {
editModeEnvironment?.wrappedValue = $0
}
}
}
private struct EditModeFixViewModifier: ViewModifier {
#Binding
var editMode: EditMode
func body(content: Content) -> some View {
content
.overlay {
EditModeFixView(editMode: $editMode)
}
}
}
I've got it to work by using a .simultaneousGesture on the EditButton and playing with a #State wrapper.
struct EditingFix: View {
#Environment(\.editMode) var editMode
#State var showDeleteButton = false
var body: some View {
Text("hello")
.toolbar(content: {
if showDeleteButton {
ToolbarItem(placement: .navigationBarLeading, content: {
Label("Remove selected", systemImage: "trash")
.foregroundColor(.red)
})
}
ToolbarItem(placement: .navigationBarTrailing, content: {
EditButton()
.simultaneousGesture(TapGesture().onEnded({
showDeleteButton.toggle()
}))
})
})
.onChange(of: showDeleteButton, perform: { isEditing in
editMode?.wrappedValue = isEditing ? .active : .inactive
})
.animation(.default, value: editMode?.wrappedValue) // Restore the default smooth animation for list selection and others
}
I can definitly say that EditButton is not using the same EditMode environment as what we get when invoking #Environment(\.editMode) var editMode. So we have to do it all ourselves if we want to get the benefit of the EditButton. Mainly the localized Edit text that it displays in my case.
Alternatively
The above method led to some weird behavior where the EditButton editMode seemed to conflict in some situation with the #Environment(\.editMode) var editMode. I'd advise you use your own logic for editing using the reliable .environment(\.editMode, $editMode). This way you can do whatever you want with the binding that control editing.
struct EditingFix: View {
#State var editMode: EditMode = .inactive
#State var isEditing = false
var body: some View {
VStack {
if editMode.isEditing {
Text("Hello")
}
Text("World")
Button("Toggle hello", action: {
isEditing.toggle()
})
}
.environment(\.editMode, $editMode)
.onChange(of: isEditing, perform: { isEditing in
editMode = isEditing ? .active : .inactive
})
.animation(.default, value: editMode)
}
}

#State var not updating SwiftUI View

I want to have my content view display data that is global to the app and manipulated outside of the content view itself.
Does swift have a binding to allow outside variables?
I have created what I think is the most basic of applications:
//
// myTestxApp.swift
// myTestx
import SwiftUI
var myStng = "Hello\n"
var myArray = ["Hello\n"]
func myTest(){
myStng.append("Hello\n")
myArray.append("Hello\n")
print(myStng,myArray)
}
#main
struct myTestxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//
// ContentView.swift
// myTestx
import SwiftUI
struct ContentView: View {
#State var i = myStng
#State var j = myArray
var body: some View {
VStack{
Button( action: myTest ){ Text("Update") }
List{ Text(i).padding() }
List{ ForEach(myArray, id: \.self)
{ i in Text(i).padding()} }
} //end VStack
} //end View
} //end ContentView
I declare two app global variables, have an external function where they are updated, and for this example, a view with a calling button to the function and List areas for the updated results tied via #State variables. In my planned app, the update functions would be part of the data processing activity. I want to be able to edit data and have the content view(s) update displayed data when that data item is updated. In this example:
Code compiles and runs, with the console showing two variables being updated, but the view controller is not responding to the state change? Is #State the appropriate binding to use or should I use some other method to cause the content view items to recognize content change?
Any pointers would be greatly appreciated.
As other fellows suggested, you have to be very careful about using global variables, you should expose them only to the scope needed.
The problem is that you are treating #State var i = myStng thinking this would create a reactive connection between i and myStng, but that is not true. This line is creating a reactive connection between i and a memory address that SwiftUI manages for you, with its first value being what myStng is at that exact moment.
Anyway, I am posting an example of how can you achieve your goal using Environment Object with your provided code.
import SwiftUI
class GlobalVariables: ObservableObject{
#Published var myStng = "Hello\n"
#Published var myArray = ["Hello\n"]
}
func myTest(variables: GlobalVariables){
variables.myStng.append("Hello\n")
variables.myArray.append("Hello\n")
}
#main
struct myTestxApp: App {
#StateObject var globalEnvironment = GlobalVariables()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(globalEnvironment)
}
}
}
//
// ContentView.swift
// myTestx
//import SwiftUI
struct ContentView: View {
#EnvironmentObject var global: GlobalVariables
var body: some View {
VStack{
Button {
myTest(variables: global)
} label: {
Text("Update")
}
List{ Text(global.myStng).padding() }
List{ ForEach(global.myArray, id: \.self)
{ i in Text(i).padding()} }
} //end VStack
} //end View
} //end ContentView

SwiftUI: resetting TabView

I have a TabView with two tabs in a SwiftUI lifecycle app, one of them has complex view structure: NavigationView with a lot of sub-views inside, i.e.: NavigationLinks and their DestinationViews are spread on multiple levels down the view tree, each sub-view on its own is another view hierarchy with sheets and / or other DestinationViews. At some point inside this hierarchy, I want to reset the TabView to its original state which is displaying the first most view, so the user can restart their journey right at that state, as they were to open the app for the first time, so it's kinda impossible to track down something like isActive & isPresented bindings to pop-off or dismiss the views and sheets.
I thought of wrapping the TabView inside another view: RootView in an attempt to find an easy way to recreate that TabView from scratch or something like refreshing / resetting the TabView, but couldn't find a clew on how to do it.
Here's my code snippet:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
struct RootView: View {
var body: some View {
ContentView()
}
}
struct ContentView: View {
var body: some View {
TabView { // <-- I need to reset it to its original state
View1() // <---- this view has complex view hierarchy
.tabItem {
Text("Home")
}.tag(0)
View2()
.tabItem {
Text("Settings")
}.tag(1)
}
}
}
p.s. I'm not looking for "popping off the view to root view", because this can't be done when there are many active NavigationLink destinations where the user might open one of the sheets and start a new navigation journey inside the sheet.
****** UPDATE ******
I've created a new Environment value to hold a boolean that should indicate whether the TabView should reset or not, and I've tracked every isPresented and isActive state variables in every view and reset them once that environment value is set to true like this:
struct ResetTabView: EnvironmentKey {
static var defaultValue: Binding<ResetTabObservable> = .constant(ResetTabObservable())
}
extension EnvironmentValues {
var resetTabView: Binding<ResetTabObservable> {
get { self[ResetTabView.self] }
set { self[ResetTabView.self] = newValue }
}
}
class ResetTabObservable: ObservableObject {
#Published var newValue = false
}
in every view that will present a sheet or push a new view I added something like this:
struct View3: View {
#State var showSheet = false
#Environment(\.resetTabView) var reset
var body: some View {
Text("This is view 3")
Button(action: {
showSheet = true
}, label: {
Text("show view 4")
})
.sheet(isPresented: $showSheet) {
View4()
}
.onReceive(reset.$newValue.wrappedValue, perform: { val in
if val == true {
showSheet = false
}
})
}
}
and in the last view (which will reset the TabView) I toggle the Environment value like this:
struct View5: View {
#Environment(\.resetTabView) var reset
var body: some View {
VStack {
Text("This is view 5")
Button(action: {
reset.newValue.wrappedValue = true
}, label: {
Text("reset tab view")
})
}
}
}
This resulted in awkward dismissal for views:
What i do for this is i make all my presentation bindings be stored using #SceneStorage("key") (instead of #State) this way they not only respect state restoration ! but you can also access them throughout your app easily by using the same key. This post gives a good example of how this enables the switching from Tab to Sidebar view on iPad.
I used this in my apps so if i have a button or something that needs to unwind many presentations it can read on all of these values and reset them back to wanted value without needing to pass around a load of bindings.

SwiftUI: ObservableObject does not persist its State over being redrawn

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).

What is the lifecycle of #State variables in SwiftUI?

If I create a new #State variable, when does it get destroyed? Does it live for the lifetime of the parent UIHostingController?
As far as I can find, it is not documented. This is relevant because I don't understand how to clean up after myself if I create an ObservableObject as State somewhere in the view hierarchy.
import SwiftUI
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
deinit {
// When will this happen?
print("Goodbye!")
}
}
Assuming:
struct Example: View {
#State private var foo = Foo()
var body: some View {
Text("My Great View")
}
}
class Foo: ObservableObject {
init() {
print(#function)
}
deinit {
print(#function)
}
}
The issue is that a View type is a struct, and it's body is not a collection of functions that are executed in real-time but actually initialized at the same time when View's body is rendered.
Problem Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
NavigationLink(destination: Example()) {
Text("Test")
}
}
}
}
If you notice, Example.init is called before the navigation even occurs, and on pop Example.deinit isn't called at all. The reason for this is that when ContentView is initialized, it has to initialize everything in it's body as well. So Example.init will be called.
When we navigate to Example, it was already initialized so Example.init is not called again. When we pop out of Example, we just go back to ContentView but since Example might be needed again, and since it is not created in real-time, it is not destroyed.
Example.deinit will be called only when ContentView has to be removed entirely.
I wasn't sure on this but found another article talking about a similar issue here:
SwiftUI and How NOT to Initialize Bindable Objects
To prove this, lets ensure the ContentView is being completely removed.
The following example makes use of an action sheet to present and remove it from the view hierarchy.
Working Scenario:
struct ContentView: View {
#State var isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() }) {
Text("Test")
}
.sheet(isPresented: $isPresented) {
Example()
.onTapGesture {
self.isPresented.toggle()
}
}
}
}
PS: This applies to classes even if not declared as #State, and does not really have anything to do with ObservableObject.
In iOS 14, the proper way to do this is to use #StateObject. There is no safe way to store a reference type in #State.