pointfreeco / episode-code-samples

💾 Point-Free episode code.
https://www.pointfree.co
MIT License
939 stars 289 forks source link

@FocusState not working with new @BindableState API #98

Closed romero-ios closed 2 years ago

romero-ios commented 2 years ago

In episode 155, the new @FocusState API was explored and an approach using the now deprecated .binding(action:) higher order reducer was demonstrated. Below is the working code snippet:

struct LoginState: Equatable {
  var focusedField: Field? = nil
  var password: String = ""
  var username: String = ""

  enum Field: String, Hashable {
    case username, password
  }
}

enum LoginAction {
  case binding(BindingAction<LoginState>)
  case signInButtonTapped
}

struct LoginEnvironment {
}

let loginReducer = Reducer<
  LoginState,
  LoginAction,
  LoginEnvironment
> { state, action, environment in
  switch action {
  case .binding:
    return .none

  case .signInButtonTapped:
    if state.username.isEmpty {
      state.focusedField = .username
    } else if state.password.isEmpty {
      state.focusedField = .password
    }
    return .none
  }
}
.binding(action: /LoginAction.binding)

struct TcaLoginView: View {
  @FocusState var focusedField: LoginState.Field?
  let store: Store<LoginState, LoginAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      VStack {
        TextField(
          "Username",
          text: viewStore.binding(keyPath: \.username, send: LoginAction.binding)
        )
          .focused($focusedField, equals: .username)

        SecureField(
          "Password",
          text: viewStore.binding(keyPath: \.password, send: LoginAction.binding)
        )
          .focused($focusedField, equals: .password)

        Button("Sign In") {
          viewStore.send(.signInButtonTapped)
        }

        Text("\(String(describing: viewStore.focusedField))")
      }
      .synchronize(
        viewStore.binding(keyPath: \.focusedField, send: LoginAction.binding),
        self.$focusedField
      )
    }
  }
}

Upgrading the code to use the new @BindableState, BindableAction API's, we get the following:

struct LoginState: Equatable {
  @BindableState var focusedField: Field? = nil
  @BindableState var password: String = ""
  @BindableState var username: String = ""

  enum Field: String, Hashable {
    case username, password
  }
}

enum LoginAction: BindableAction {
  case binding(BindingAction<LoginState>)
  case signInButtonTapped
}

struct LoginEnvironment {
}

let loginReducer = Reducer<
  LoginState,
  LoginAction,
  LoginEnvironment
> { state, action, environment in
  switch action {

  case .binding(\.$focusedField):
    return .none

  case .binding:
    return .none

  case .signInButtonTapped:
    if state.username.isEmpty {
      state.focusedField = .username
    } else if state.password.isEmpty {
      state.focusedField = .password
    }
    return .none
  }
}

struct TcaLoginView: View {
  @FocusState var focusedField: LoginState.Field?
  let store: Store<LoginState, LoginAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      VStack {
        TextField(
          "Username",
          text: viewStore.binding(\.$username)
        )
          .focused($focusedField, equals: .username)

        SecureField(
          "Password",
          text: viewStore.binding(\.$password)
        )
          .focused($focusedField, equals: .password)

        Button("Sign In") {
          viewStore.send(.signInButtonTapped)
        }

        Text("\(String(describing: viewStore.focusedField))")
      }
      .synchronize(viewStore.binding(\.$focusedField), self.$focusedField)
    }
  }
}

When running the application, tapping on a TextField no longer updates the Text view that displays the name of the focused field.

gohanlon commented 2 years ago

I haven't tried running your upgraded @BindableState code, but I read it over and noticed that you didn't call binding on your upgraded reducer:

let loginReducer = Reducer<...> { state, action, environment in
  ...
}
.binding() // ← 

Hope that helps!

romero-ios commented 2 years ago

Oh wow! I hadn't realized I missed that. It's working now - thank you @gohanlon

Ryu0118 commented 1 year ago

Hi, I'm trying to make FocusState correspond to BindableState as well.

.synchronize(viewStore.binding(\. $focusedField), self.$focusedField)

I can't find this modifier, if it has been renamed or if a library is needed, could you please tell me about it?

atecle commented 1 year ago

Hey @Ryu0118, it's defined in the case study about FocusState

stephencelis commented 1 year ago

Just a note, but this helper ships as a method called View.bind in our SwiftUI Navigation library: https://pointfreeco.github.io/swiftui-navigation/main/documentation/swiftuinavigation/bindings/