diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift index 312fcefff0bdd6eec609db0dc81e180032368db2..c3ceb282cfb093a41828e7e3e76fbd6f497401a7 100644 --- a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift +++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift @@ -6,23 +6,34 @@ import XXModels public struct NewGroupComponent: ReducerProtocol { public struct State: Equatable { + public enum Field: String, Hashable { + case name + } + public init( contacts: IdentifiedArrayOf<XXModels.Contact> = [], - members: IdentifiedArrayOf<XXModels.Contact> = [] + members: IdentifiedArrayOf<XXModels.Contact> = [], + name: String = "", + focusedField: Field? = nil ) { self.contacts = contacts self.members = members + self.name = name + self.focusedField = focusedField } public var contacts: IdentifiedArrayOf<XXModels.Contact> public var members: IdentifiedArrayOf<XXModels.Contact> + @BindableState public var name: String + @BindableState public var focusedField: Field? } - public enum Action: Equatable { + public enum Action: Equatable, BindableAction { case start case didFetchContacts([XXModels.Contact]) case didSelectContact(XXModels.Contact) case didFinish + case binding(BindingAction<State>) } public init() {} @@ -32,34 +43,40 @@ public struct NewGroupComponent: ReducerProtocol { @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> - public func reduce(into state: inout State, action: Action) -> EffectTask<Action> { - switch action { - case .start: - let myId = try? messenger.e2e.tryGet().getContact().getId() - return Effect - .catching { try db() } - .flatMap { $0.fetchContactsPublisher(.init()) } - .assertNoFailure() - .map { $0.filter { $0.id != myId } } - .map(Action.didFetchContacts) - .subscribe(on: bgQueue) - .receive(on: mainQueue) - .eraseToEffect() + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .start: + let myId = try? messenger.e2e.tryGet().getContact().getId() + return Effect + .catching { try db() } + .flatMap { $0.fetchContactsPublisher(.init()) } + .assertNoFailure() + .map { $0.filter { $0.id != myId } } + .map(Action.didFetchContacts) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() - case .didFetchContacts(let contacts): - state.contacts = IdentifiedArray(uniqueElements: contacts) - return .none + case .didFetchContacts(let contacts): + state.contacts = IdentifiedArray(uniqueElements: contacts) + return .none - case .didSelectContact(let contact): - if state.members.contains(contact) { - state.members.remove(contact) - } else { - state.members.append(contact) - } - return .none + case .didSelectContact(let contact): + if state.members.contains(contact) { + state.members.remove(contact) + } else { + state.members.append(contact) + } + return .none - case .didFinish: - return .none + case .didFinish: + return .none + + case .binding(_): + return .none + } } } } diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift index 5540743b752653ca849731b8fbb00968a3b1b59b..697fdd17d46ec026f5d552a141180b295dbc115f 100644 --- a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift +++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift @@ -12,15 +12,20 @@ public struct NewGroupView: View { } let store: StoreOf<Component> + @FocusState var focusedField: Component.State.Field? struct ViewState: Equatable { init(state: Component.State) { contacts = state.contacts members = state.members + name = state.name + focusedField = state.focusedField } var contacts: IdentifiedArrayOf<XXModels.Contact> var members: IdentifiedArrayOf<XXModels.Contact> + var name: String + var focusedField: Component.State.Field? } public var body: some View { @@ -28,10 +33,13 @@ public struct NewGroupView: View { Form { Section { membersView(viewStore) + nameView(viewStore) } } .navigationTitle("New Group") .task { viewStore.send(.start) } + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } } } @@ -54,6 +62,14 @@ public struct NewGroupView: View { } } } + + func nameView(_ viewStore: ViewStore) -> some View { + TextField("Group name", text: viewStore.binding( + get: \.name, + send: { .set(\.$name, $0) } + )) + .focused($focusedField, equals: .name) + } } #if DEBUG diff --git a/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift index 45a71ad597f5a2b17b16cc3ade85fb03588f8913..d8d742ba1188a44f60e5741a0a5191ac1bb3b81d 100644 --- a/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift +++ b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift @@ -97,6 +97,25 @@ final class NewGroupComponentTests: XCTestCase { } } + func testEnterGroupName() { + let store = TestStore( + initialState: NewGroupComponent.State(), + reducer: NewGroupComponent() + ) + + store.send(.binding(.set(\.$focusedField, .name))) { + $0.focusedField = .name + } + + store.send(.binding(.set(\.$name, "My New Group"))) { + $0.name = "My New Group" + } + + store.send(.binding(.set(\.$focusedField, nil))) { + $0.focusedField = nil + } + } + func testFinish() { let store = TestStore( initialState: NewGroupComponent.State(),