From 08454d98ce1c5daab94cc30014d6c4ede993784b Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Fri, 21 Oct 2022 12:43:34 +0200 Subject: [PATCH] Migrate UserSearchFeature to ReducerProtocol --- .../UserSearchComponent.swift | 146 +++++++++++++++ .../UserSearchFeature/UserSearchFeature.swift | 168 ------------------ .../UserSearchFeature/UserSearchView.swift | 25 ++- ...s.swift => UserSearchComponentTests.swift} | 38 ++-- 4 files changed, 175 insertions(+), 202 deletions(-) create mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift delete mode 100644 Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift rename Examples/xx-messenger/Tests/UserSearchFeatureTests/{UserSearchFeatureTests.swift => UserSearchComponentTests.swift} (81%) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift new file mode 100644 index 00000000..c274d15b --- /dev/null +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchComponent.swift @@ -0,0 +1,146 @@ +import ComposableArchitecture +import ComposablePresentation +import ContactFeature +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct UserSearchComponent: ReducerProtocol { + public struct State: Equatable { + public enum Field: String, Hashable { + case username + case email + case phone + } + + public struct Result: Equatable, Identifiable { + public init( + id: Data, + xxContact: XXClient.Contact, + username: String? = nil, + email: String? = nil, + phone: String? = nil + ) { + self.id = id + self.xxContact = xxContact + self.username = username + self.email = email + self.phone = phone + } + + public var id: Data + public var xxContact: XXClient.Contact + public var username: String? + public var email: String? + public var phone: String? + + public var hasFacts: Bool { + username != nil || email != nil || phone != nil + } + } + + public init( + focusedField: Field? = nil, + query: MessengerSearchContacts.Query = .init(), + isSearching: Bool = false, + failure: String? = nil, + results: IdentifiedArrayOf<Result> = [], + contact: ContactComponent.State? = nil + ) { + self.focusedField = focusedField + self.query = query + self.isSearching = isSearching + self.failure = failure + self.results = results + self.contact = contact + } + + @BindableState public var focusedField: Field? + @BindableState public var query: MessengerSearchContacts.Query + public var isSearching: Bool + public var failure: String? + public var results: IdentifiedArrayOf<Result> + public var contact: ContactComponent.State? + } + + public enum Action: Equatable, BindableAction { + case searchTapped + case didFail(String) + case didSucceed([Contact]) + case didDismissContact + case resultTapped(id: Data) + case binding(BindingAction<State>) + case contact(ContactComponent.Action) + } + + public init() {} + + @Dependency(\.app.messenger) var messenger: Messenger + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + + public var body: some ReducerProtocol<State, Action> { + BindingReducer() + Reduce { state, action in + switch action { + case .searchTapped: + state.focusedField = nil + state.isSearching = true + state.results = [] + state.failure = nil + return .result { [query = state.query] in + do { + return .success(.didSucceed(try messenger.searchContacts(query: query))) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didSucceed(let contacts): + state.isSearching = false + state.failure = nil + state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in + guard let id = try? contact.getId() else { return nil } + return State.Result( + id: id, + xxContact: contact, + username: try? contact.getFact(.username)?.value, + email: try? contact.getFact(.email)?.value, + phone: try? contact.getFact(.phone)?.value + ) + }) + return .none + + case .didFail(let failure): + state.isSearching = false + state.failure = failure + state.results = [] + return .none + + case .didDismissContact: + state.contact = nil + return .none + + case .resultTapped(let id): + state.contact = ContactComponent.State( + id: id, + xxContact: state.results[id: id]?.xxContact + ) + return .none + + case .binding(_), .contact(_): + return .none + } + } + .presenting( + state: .keyPath(\.contact), + id: .keyPath(\.?.id), + action: /Action.contact, + presented: { ContactComponent() } + ) + } +} diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift deleted file mode 100644 index f39353a7..00000000 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchFeature.swift +++ /dev/null @@ -1,168 +0,0 @@ -import ComposableArchitecture -import ComposablePresentation -import ContactFeature -import Foundation -import XCTestDynamicOverlay -import XXClient -import XXMessengerClient - -public struct UserSearchState: Equatable { - public enum Field: String, Hashable { - case username - case email - case phone - } - - public struct Result: Equatable, Identifiable { - public init( - id: Data, - xxContact: XXClient.Contact, - username: String? = nil, - email: String? = nil, - phone: String? = nil - ) { - self.id = id - self.xxContact = xxContact - self.username = username - self.email = email - self.phone = phone - } - - public var id: Data - public var xxContact: XXClient.Contact - public var username: String? - public var email: String? - public var phone: String? - - public var hasFacts: Bool { - username != nil || email != nil || phone != nil - } - } - - public init( - focusedField: Field? = nil, - query: MessengerSearchContacts.Query = .init(), - isSearching: Bool = false, - failure: String? = nil, - results: IdentifiedArrayOf<Result> = [], - contact: ContactState? = nil - ) { - self.focusedField = focusedField - self.query = query - self.isSearching = isSearching - self.failure = failure - self.results = results - self.contact = contact - } - - @BindableState public var focusedField: Field? - @BindableState public var query: MessengerSearchContacts.Query - public var isSearching: Bool - public var failure: String? - public var results: IdentifiedArrayOf<Result> - public var contact: ContactState? -} - -public enum UserSearchAction: Equatable, BindableAction { - case searchTapped - case didFail(String) - case didSucceed([Contact]) - case didDismissContact - case resultTapped(id: Data) - case binding(BindingAction<UserSearchState>) - case contact(ContactAction) -} - -public struct UserSearchEnvironment { - public init( - messenger: Messenger, - mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue>, - contact: @escaping () -> ContactEnvironment - ) { - self.messenger = messenger - self.mainQueue = mainQueue - self.bgQueue = bgQueue - self.contact = contact - } - - public var messenger: Messenger - public var mainQueue: AnySchedulerOf<DispatchQueue> - public var bgQueue: AnySchedulerOf<DispatchQueue> - public var contact: () -> ContactEnvironment -} - -#if DEBUG -extension UserSearchEnvironment { - public static let unimplemented = UserSearchEnvironment( - messenger: .unimplemented, - mainQueue: .unimplemented, - bgQueue: .unimplemented, - contact: { .unimplemented } - ) -} -#endif - -public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSearchEnvironment> -{ state, action, env in - switch action { - case .searchTapped: - state.focusedField = nil - state.isSearching = true - state.results = [] - state.failure = nil - return .result { [query = state.query] in - do { - return .success(.didSucceed(try env.messenger.searchContacts(query: query))) - } catch { - return .success(.didFail(error.localizedDescription)) - } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - - case .didSucceed(let contacts): - state.isSearching = false - state.failure = nil - state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in - guard let id = try? contact.getId() else { return nil } - return UserSearchState.Result( - id: id, - xxContact: contact, - username: try? contact.getFact(.username)?.value, - email: try? contact.getFact(.email)?.value, - phone: try? contact.getFact(.phone)?.value - ) - }) - return .none - - case .didFail(let failure): - state.isSearching = false - state.failure = failure - state.results = [] - return .none - - case .didDismissContact: - state.contact = nil - return .none - - case .resultTapped(let id): - state.contact = ContactState( - id: id, - xxContact: state.results[id: id]?.xxContact - ) - return .none - - case .binding(_), .contact(_): - return .none - } -} -.binding() -.presenting( - contactReducer, - state: .keyPath(\.contact), - id: .keyPath(\.?.id), - action: /UserSearchAction.contact, - environment: { $0.contact() } -) diff --git a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift index 328ff98c..ea5f2ad7 100644 --- a/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift +++ b/Examples/xx-messenger/Sources/UserSearchFeature/UserSearchView.swift @@ -5,21 +5,21 @@ import SwiftUI import XXMessengerClient public struct UserSearchView: View { - public init(store: Store<UserSearchState, UserSearchAction>) { + public init(store: StoreOf<UserSearchComponent>) { self.store = store } - let store: Store<UserSearchState, UserSearchAction> - @FocusState var focusedField: UserSearchState.Field? + let store: StoreOf<UserSearchComponent> + @FocusState var focusedField: UserSearchComponent.State.Field? struct ViewState: Equatable { - var focusedField: UserSearchState.Field? + var focusedField: UserSearchComponent.State.Field? var query: MessengerSearchContacts.Query var isSearching: Bool var failure: String? - var results: IdentifiedArrayOf<UserSearchState.Result> + var results: IdentifiedArrayOf<UserSearchComponent.State.Result> - init(state: UserSearchState) { + init(state: UserSearchComponent.State) { focusedField = state.focusedField query = state.query isSearching = state.isSearching @@ -35,7 +35,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.username ?? "" }, - send: { UserSearchAction.set(\.$query.username, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.username, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter username"), label: { Text("Username") } @@ -45,7 +45,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.email ?? "" }, - send: { UserSearchAction.set(\.$query.email, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.email, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter email"), label: { Text("Email") } @@ -55,7 +55,7 @@ public struct UserSearchView: View { TextField( text: viewStore.binding( get: { $0.query.phone ?? "" }, - send: { UserSearchAction.set(\.$query.phone, $0.isEmpty ? nil : $0) } + send: { UserSearchComponent.Action.set(\.$query.phone, $0.isEmpty ? nil : $0) } ), prompt: Text("Enter phone"), label: { Text("Phone") } @@ -124,7 +124,7 @@ public struct UserSearchView: View { .background(NavigationLinkWithStore( store.scope( state: \.contact, - action: UserSearchAction.contact + action: UserSearchComponent.Action.contact ), onDeactivate: { viewStore.send(.didDismissContact) }, destination: ContactView.init(store:) @@ -137,9 +137,8 @@ public struct UserSearchView: View { public struct UserSearchView_Previews: PreviewProvider { public static var previews: some View { UserSearchView(store: Store( - initialState: UserSearchState(), - reducer: .empty, - environment: () + initialState: UserSearchComponent.State(), + reducer: EmptyReducer() )) } } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift similarity index 81% rename from Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift rename to Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift index 33f1edb9..97ca5fae 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchComponentTests.swift @@ -6,12 +6,11 @@ import XXClient import XXMessengerClient @testable import UserSearchFeature -final class UserSearchFeatureTests: XCTestCase { +final class UserSearchComponentTests: XCTestCase { func testSearch() { let store = TestStore( - initialState: UserSearchState(), - reducer: userSearchReducer, - environment: .unimplemented + initialState: UserSearchComponent.State(), + reducer: UserSearchComponent() ) var didSearchWithQuery: [MessengerSearchContacts.Query] = [] @@ -43,9 +42,9 @@ final class UserSearchFeatureTests: XCTestCase { contact4.getFactsFromContact.run = { _ in throw GetFactsFromContactError() } let contacts = [contact1, contact2, contact3, contact4] - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.searchContacts.run = { query in + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.searchContacts.run = { query in didSearchWithQuery.append(query) return contacts } @@ -93,17 +92,16 @@ final class UserSearchFeatureTests: XCTestCase { func testSearchFailure() { let store = TestStore( - initialState: UserSearchState(), - reducer: userSearchReducer, - environment: .unimplemented + initialState: UserSearchComponent.State(), + reducer: UserSearchComponent() ) struct Failure: Error {} let failure = Failure() - store.environment.bgQueue = .immediate - store.environment.mainQueue = .immediate - store.environment.messenger.searchContacts.run = { _ in throw failure } + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.messenger.searchContacts.run = { _ in throw failure } store.send(.searchTapped) { $0.focusedField = nil @@ -121,7 +119,7 @@ final class UserSearchFeatureTests: XCTestCase { func testResultTapped() { let store = TestStore( - initialState: UserSearchState( + initialState: UserSearchComponent.State( results: [ .init( id: "contact-id".data(using: .utf8)!, @@ -129,12 +127,11 @@ final class UserSearchFeatureTests: XCTestCase { ) ] ), - reducer: userSearchReducer, - environment: .unimplemented + reducer: UserSearchComponent() ) store.send(.resultTapped(id: "contact-id".data(using: .utf8)!)) { - $0.contact = ContactState( + $0.contact = ContactComponent.State( id: "contact-id".data(using: .utf8)!, xxContact: .unimplemented("contact-data".data(using: .utf8)!) ) @@ -143,13 +140,12 @@ final class UserSearchFeatureTests: XCTestCase { func testDismissingContact() { let store = TestStore( - initialState: UserSearchState( - contact: ContactState( + initialState: UserSearchComponent.State( + contact: ContactComponent.State( id: "contact-id".data(using: .utf8)! ) ), - reducer: userSearchReducer, - environment: .unimplemented + reducer: UserSearchComponent() ) store.send(.didDismissContact) { -- GitLab