How to associate a block of code with a @State var when it changes

Viewed 118

Whenever the value of the @State variable myData changes I would like to be notified and store that data in an @AppStorage variable myStoredData. However, I don't want to have to write explicitly this storing code everywhere the state var is changed, I would like to associate a block of code with it that gets notified whenever the state var changes and performs storage. The reason for this is, for example, I want to pass the state var as a binding to another view, and when that view would change the variable, the storage block would automatically be executed. How can I do this/can I do this in SwiftUI?

struct MyView : View {
  @AppStorage("my-data") var myStoredData : Data!
  @State var myData : [String] = ["hello","world"]
  var body : some View {
    Button(action: {
      myData = ["something","else"]
      myStoredData = try? JSONEncoder().encode(myData)
    }) {
      Text("Store data when button pressed")
    }
    .onAppear {
        myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
    }
  }
}

I'm looking for something like this, but this does not work:

@State var myData : [String] = ["hello","world"] {
  didSet {
    myStoredData = try? JSONEncoder().encode(myData)
  }
}
2 Answers

Simply use .onChange modifier anywhere in view (like you did with .onAppear)

struct MyView : View {
  @AppStorage("my-data") var myStoredData : Data!
  @State var myData : [String] = ["hello","world"]

  var body : some View {
    Button(action: {
      myData = ["something","else"]
      myStoredData = try? JSONEncoder().encode(myData)
    }) {
      Text("Store data when button pressed")
    }
    .onChange(of: myData) {
        myStoredData = try? JSONEncoder().encode($0)      // << here !!
    }
    .onAppear {
        myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
    }
  }
}

You can add a set callback extension to the binding to monitor the value change

extension Binding {

    /// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
    /// - Parameter closure: Chunk of code to execute whenever the value changes.
    /// - Returns: New `Binding`.
    func onChange(_ closure: @escaping () -> Void) -> Binding<Value> {
        Binding(get: {
            wrappedValue
        }, set: { newValue in
            wrappedValue = newValue
            closure()
        })
    }
}

Use extension code

$myData.onChange({

})
Related