pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.35k stars 1.44k forks source link

TextField binding not updating on iOS17 #2714

Closed marioradonic closed 9 months ago

marioradonic commented 9 months ago

Description

In the case where BindingState is updated directly in the action that updated it the TextField value is not updated. Tested with SCA 1.6.0 on iOS17 where it happens, and iOS16 where it doesn't happen.

Minimal reproducible example:

import SwiftUI
import ComposableArchitecture

@main
struct TestBindingApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().task {
                runTest()
            }
        }
    }
}

@Reducer
struct SomeReducer {
    struct State: Equatable {
        @BindingState var text = ""
    }
    enum Action: BindableAction {
        case binding(BindingAction<State>)
    }

    var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .binding(\.$text):
                if state.text == "123" {
                    state.text = ""
                }
            case .binding:
                break
            }
            return .none
        }
    }
}

struct ViewState: Equatable {
    @BindingViewState var text: String

    init(state: BindingViewStore<SomeReducer.State>) {
        self._text = state.$text
    }
}

struct ContentView: View {
    let store = Store(initialState: .init()) {
        SomeReducer()._printChanges()
    }

    var body: some View {
        WithViewStore(store, observe: ViewState.init) { viewStore in
            VStack {
                Text(viewStore.text)
                TextField("Title", text: viewStore.$text).textFieldStyle(.plain)
            }
        }
    }
}

Checklist

Expected behavior

In the above example I would expect both Text and TextField values to reset to empty string if I type in "123".

Actual behavior

The value of the Text updates, but the value of the TextField stays "123".

https://github.com/pointfreeco/swift-composable-architecture/assets/4158737/a6ac8bb7-7975-48fd-8ed7-caccb17bde45

Steps to reproduce

No response

The Composable Architecture version information

1.6.0

Destination operating system

iOS 17

Xcode version information

15.2

Swift Compiler version information

swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx13.0
stephencelis commented 9 months ago

@marioradonic How did you rule out this behavior in vanilla SwiftUI? If I use a vanilla observable object instead of a store I can reproduce the exact same behavior:

class Model: ObservableObject {
  @Published var text = "" {
    didSet {
      if text == "123" {
        text = ""
      }
    }
  }
}

struct ContentView: View {
  @ObservedObject var model = Model()

  var body: some View {
    VStack {
      Text(model.text)
      TextField("Title", text: $model.text).textFieldStyle(.plain)
    }
  }
}

Because this appears to be a vanilla SwiftUI behavior and not a bug in TCA, I'm going to convert to a discussion, where workarounds may be discussed.