27

Normally we can use didSet in swift to monitor the updates of a variable. But it didn't work for a @Binding variable. For example, I have the following code:

@Binding var text {
   didSet {
       ......
   }
}

But the didSet is never been called.Any idea? Thanks.

Bagusflyer
  • 12,675
  • 21
  • 96
  • 179
  • Could you provide more code? use `didSet` all the time. The issue is with code you haven't presented. –  Sep 16 '19 at 10:18
  • 10
    DidSet doesn’t get called on @Binding because the binding doesn’t get set. It’s the value wrapped in binding that changes. Can you explain what you are trying to achieve in didSet? – LuLuGaGa Sep 16 '19 at 10:35
  • 1
    Look at this answer: https://stackoverflow.com/questions/56550713/how-can-i-run-an-action-when-a-state-changes/56581087#56581087 – Peter Pohlmann Sep 17 '19 at 15:52
  • 1
    Is it a `UIViewRepresentable`? Then you can use `updateUIView`. `updateUIView` is called every time a binding or state changes. – Johannes Apr 14 '20 at 10:30

3 Answers3

23

Instead of didSet you can always use onReceive (iOS 13+) or onChange (iOS 14+):

import Combine
import SwiftUI

struct ContentView: View {
    @State private var counter = 1
    
    var body: some View {
        ChildView(counter: $counter)
        Button("Increment") {
            counter += 1
        }
    }
}

struct ChildView: View {
    @Binding var counter: Int
    
    var body: some View {
        Text(String(counter))
            .onReceive(Just(counter)) { value in
                print("onReceive: \(value)")
            }
            .onChange(of: counter) { value in
                print("onChange: \(value)")
            }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
13

You shouldn’t need a didSet observer on a @Binding.

If you want a didSet because you want to compute something else for display when text changes, just compute it. For example, if you want to display the count of characters in text:

struct ContentView: View {
    @Binding var text: String

    var count: Int { text.count }

    var body: some View {
        VStack {
            Text(text)
            Text(“count: \(count)”)
        }
    }
}

If you want to observe text because you want to make some other change to your data model, then observing the change from your View is wrong. You should be observing the change from elsewhere in your model, or in a controller object, not from your View. Remember that your View is a value type, not a reference type. SwiftUI creates it when needed, and might store multiple copies of it, or no copies at all.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    Thanks for your reply. I know what you mean. And it works in `body` function. But in my case, I'm not inheriting from a `View`. Instead I'm inheriting from a `UIViewRepresentable`. It's easier for me if `didSet` works. – Bagusflyer Sep 18 '19 at 04:01
  • 1
    `UIViewRepresentable` is a sub-protocol of `View`. Any type that conforms to `UIViewRepresentable` conforms to `View`. – rob mayoff Sep 18 '19 at 04:22
  • This does not allow for a case where you want to refresh the view when the value of the bound variable changes. In the above snippet, "count" would need to be Published, but you cannot have the Published wrapper on a computed property. – Andy Thomas Mar 10 '20 at 15:59
  • (Re: my earlier comment, I am referring to a case where the "text" Binding variable and the "count" computed variable are managed within a ViewModel) – Andy Thomas Mar 10 '20 at 16:14
  • No, `count` does not need to be `Published`, because it is computed on demand entirely from values tracked by SwiftUI. The `count` getter calls the `text` getter, so when the program accesses `count`, SwiftUI notices the dependency on `text`. – rob mayoff Mar 10 '20 at 16:22
-1

The best way is to wrap the property in an ObservableObject:

final class TextStore: ObservableObject {
    @Published var text: String = "" {
        didSet { ... }
   }
}

And then use that ObservableObject's property as a binding variable in your view:

struct ContentView: View {
    @ObservedObject var store = TextStore()
    var body: some View {
        TextField("", text: $store.text)
    }
}

didSet will now be called whenever text changes.


Alternatively, you could create a sort of makeshift Binding value:

TextField("", text: Binding<String>(
    get: {
        return self.text
    },
    set: { newValue in
        self.text = newValue
        ...
    }
))

Just note that with this second strategy, the get function will be called every time the view is updated. I wouldn't recommend using this approach, but nevertheless it's good to be aware of it.

McKinley
  • 1,123
  • 1
  • 8
  • 18