3

I have an ObservableObject which is supposed to hold my application state:

final class Store: ObservableObject {
  @Published var fetchInterval = 30
}

now, that object is being in injected at the root of my hierarchy and then at some component down the tree I'm trying to access it and bind it to a TextField, namely:

struct ConfigurationView: View {
  @EnvironmnetObject var store: Store

  var body: some View {
    TextField("Fetch interval", $store.fetchInterval, formatter: NumberFormatter())
    Text("\(store.fetchInterval)"
  }
}
  1. Even though the variable is binded (with $), the property is not being updated, the initial value is displayed correctly but when I change it, the textfield changes but the binding is not propagated
  2. Related to the first question, is, how would I receive an event once the value is changed, I tried the following snippet, but nothing is getting fired (I assume because the textfield is not correctly binded...
$fetchInterval
           .debounce(for: 0.8, scheduler: RunLoop.main)
           .removeDuplicates()
           .sink { interval in
               print("sink from my code \(interval)")
           }

Any help is much appreciated.

Edit: I just discovered that for text variables, the binding works fine out of the box, ex:

// on store
@Published var testString = "ropo"

// on component
TextField("Ropo", text: $store.testString)
Text("\(store.testString)")

it is only on the int field that it does not update the variable correctly

Edit 2: Ok I have just discovered that only changing the field is not enough, one has to press Enter for the change to propagate, which is not what I want, I want the changes to propagate every time the field is changed...

Oscar Franco
  • 5,691
  • 5
  • 34
  • 56

3 Answers3

5

For anyone that is interested, this is te solution I ended up with:

TextField("Seconds", text: Binding(
                    get: { String(self.store.fetchInterval) },
                    set: { self.store.fetchInterval = Int($0.filter { "0123456789".contains($0) }) ?? self.store.fetchInterval }
                ))

There is a small delay when a non-valid character is added, but it is the cleanest solution that does not allow for invalid characters without having to reset the state of the TextField.

It also immediately commits changes without having to wait for user to press enter or without having to wait for the field to blur.

Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
2

Do it like this and you don't even have to press enter. This would work with EnvironmentObject too, if you put Store() in SceneDelegate:

struct ContentView: View {
@ObservedObject var store = Store()


var body: some View {
    VStack {
        TextField("Fetch interval", text: $store.fetchInterval)
    Text("\(store.fetchInterval)")
    }
} }

Concerning your 2nd question: In SwiftUI a view gets always updated automatically if a variable in it changes.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Kuhlemann
  • 3,066
  • 3
  • 14
  • 41
  • Hmm yeah, just using text directly changes the observed value, but what about validation? it will just try to put text in there right? – Oscar Franco Apr 19 '20 at 18:33
  • If you only use your App on iPhones, the simplest solution for that would be to add " .keyboardType(.numberPad)" under your TextField. Note that on the simulator you will have to have the keypad open (CMD+K) after clicking in the TextField to simulate this behavior correctly. For a more complex solution take a look at this: https://stackoverflow.com/questions/58733003/swift-ui-how-to-create-textfield-that-accepts-numbers-only/58736068#58736068 – Kuhlemann Apr 19 '20 at 18:39
  • no, unfortunately (?) I'm developing a mac app, so allowing for text values is more of a headache – Oscar Franco Apr 19 '20 at 18:53
  • this code does not compile. TextField takes Binding as parameter. It does not take an Int. – workingdog support Ukraine Apr 20 '20 at 02:45
1

how about a simple solution that works well on macos as well, like this:

import SwiftUI

final class Store: ObservableObject {
    @Published var fetchInterval: Int = 30
    }

    struct ContentView: View {
       @ObservedObject var store = Store()
       var body: some View {
           VStack{
               TextField("Fetch interval", text: Binding<String>(
                   get: { String(format: "%d", self.store.fetchInterval) },
                   set: {
                       if let value = NumberFormatter().number(from: $0) {
                           self.store.fetchInterval = value.intValue
                       }}))
               Text("\(store.fetchInterval)").padding()
           }
     }
}
Rohit Makwana
  • 4,337
  • 1
  • 21
  • 29