2

I have a SwiftUI View where I declare a condition like this

@State var condition = UserDefaults.standard.bool(forKey: "JustKey")

When I first push this view into the navigation stack condition variable is getting the correct value. Then when I pop this View I change the value in UserDefaults but when I push this screen again condition variable remembers the old value which it got first time. How to find a workaround for this because I want to reinitialize my condition variable each time I enter my custom view where I declared it?

Karen Karapetyan
  • 704
  • 1
  • 9
  • 18
  • As a workaround you could revaluate the state using `onAppear` method of the view. I have to emphasis that this is indeed a workaround and there might be a better way to do so. – EvZ Dec 19 '19 at 10:13
  • As we don't know, at least I don't know, how **SwiftUI** manages its special `State` (or `ObservedObject`) objects yet, we can't say much about the de-initialization process of the variables. But as far as **Apple** guides us on _how to control the data flow of the source of truth_ it can be deduced that you have a single access point of your data. That being said, you can try [the approach discussed here](https://stackoverflow.com/questions/56822195/how-do-i-use-userdefaults-with-swiftui) – nayem Dec 19 '19 at 10:48
  • @EvZ I also came to that solution but unfortunately when I set the value in onAppear the app crashes bc based on that value I add a section in List and xcode complains saying Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy... – Karen Karapetyan Dec 19 '19 at 12:35

1 Answers1

2

In this case, @State is behaving exactly like it is supposed to: as a persistent store for the component it's attached to.

Fundamentally, pushing a view with a NavigationLink is like any other component in the view hierarchy. Whether or not the screen is actually visible is an implementation detail. While SwiftUI is not actually rendering your hidden UI elements after closing a screen, it does hold on to the View tree.

You can force a view to be completely thrown away with the .id(_:) modifier, for example:

struct ContentView: View {
  @State var i = 0

  var body: some View {
    NavigationView {
      VStack {
        NavigationLink(destination: DetailView().id(i)) {
          Text("Show Detail View")
        }
        Button("Toggle") {
          self.i += 1
          UserDefaults.standard.set(
            !UserDefaults.standard.bool(forKey: "JustKey"),
            forKey: "JustKey")
        }
      }
    }
  }
}

The toggle button both modifies the value for JustKey and increments the value that we pass to .id(i). When the singular argument to id(_:) changes, the id modifier tells SwiftUI that this is a different view, so when SwiftUI runs it's diffing algorithm it throws away the old one and creates a new one (with new @State variables). Read more about id here.


The explanation above provides a workaround, but it's not a good solution, IMO. A much better solution is to use an ObservableObject. It looks like this:

@ObservedObject condition = KeyPathObserver(\.JustKey, on: UserDefaults.standard)

You can find the code that implements the KeyPathObserver and a full working Playground example at this SO answer

Gil Birman
  • 35,242
  • 14
  • 75
  • 119