From 2708f07f4ee6aac961257f3ce1e48b8abc0d0b8e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 13:00:35 +0200 Subject: [PATCH 1/5] Add ContactsFeature library --- .../xcschemes/ContactsFeature.xcscheme | 78 +++++++++++++++++++ Examples/xx-messenger/Package.swift | 18 +++++ .../xcschemes/XXMessenger.xcscheme | 10 +++ .../ContactsFeature/ContactsFeature.swift | 28 +++++++ .../ContactsFeature/ContactsView.swift | 38 +++++++++ .../ContactsFeatureTests.swift | 15 ++++ 6 files changed, 187 insertions(+) create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme create mode 100644 Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift create mode 100644 Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift create mode 100644 Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme new file mode 100644 index 00000000..feb1a490 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactsFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1400" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactsFeature" + BuildableName = "ContactsFeature" + BlueprintName = "ContactsFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactsFeatureTests" + BuildableName = "ContactsFeatureTests" + BlueprintName = "ContactsFeatureTests" + ReferencedContainer = "container:"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactsFeature" + BuildableName = "ContactsFeature" + BlueprintName = "ContactsFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 77b97a0f..54f5035c 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "AppCore", targets: ["AppCore"]), .library(name: "AppFeature", targets: ["AppFeature"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), + .library(name: "ContactsFeature", targets: ["ContactsFeature"]), .library(name: "HomeFeature", targets: ["HomeFeature"]), .library(name: "RegisterFeature", targets: ["RegisterFeature"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), @@ -111,6 +112,23 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "ContactsFeature", + dependencies: [ + .target(name: "AppCore"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "ComposablePresentation", package: "swift-composable-presentation"), + .product(name: "XXModels", package: "client-ios-db"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ContactsFeatureTests", + dependencies: [ + .target(name: "ContactsFeature"), + ], + swiftSettings: swiftSettings + ), .target( name: "HomeFeature", dependencies: [ diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme index 0d7cb106..9d31f6f7 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -59,6 +59,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactsFeatureTests" + BuildableName = "ContactsFeatureTests" + BlueprintName = "ContactsFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift new file mode 100644 index 00000000..e7f98fe7 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTestDynamicOverlay + +public struct ContactsState: Equatable { + public init() {} +} + +public enum ContactsAction: Equatable { + case start +} + +public struct ContactsEnvironment { + public init() {} +} + +#if DEBUG +extension ContactsEnvironment { + public static let unimplemented = ContactsEnvironment() +} +#endif + +public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvironment> +{ state, action, env in + switch action { + case .start: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift new file mode 100644 index 00000000..870889f2 --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +import SwiftUI + +public struct ContactsView: View { + public init(store: Store<ContactsState, ContactsAction>) { + self.store = store + } + + let store: Store<ContactsState, ContactsAction> + + struct ViewState: Equatable { + init(state: ContactsState) {} + } + + public var body: some View { + WithViewStore(store.scope(state: ViewState.init)) { viewStore in + Form { + + } + .navigationTitle("Contacts") + .task { viewStore.send(.start) } + } + } +} + +#if DEBUG +public struct ContactsView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + ContactsView(store: Store( + initialState: ContactsState(), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift new file mode 100644 index 00000000..3b521768 --- /dev/null +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import XCTest +@testable import ContactsFeature + +final class ContactsFeatureTests: XCTestCase { + func testStart() { + let store = TestStore( + initialState: ContactsState(), + reducer: contactsReducer, + environment: .unimplemented + ) + + store.send(.start) + } +} -- GitLab From 42d12de2743b12d44a9843a59f515269b21812ed Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 13:10:08 +0200 Subject: [PATCH 2/5] Present Contacts from Home --- Examples/xx-messenger/Package.swift | 2 ++ .../AppFeature/AppEnvironment+Live.swift | 4 +++ .../Sources/HomeFeature/HomeFeature.swift | 28 ++++++++++++++++++- .../Sources/HomeFeature/HomeView.swift | 21 ++++++++++++++ .../HomeFeatureTests/HomeFeatureTests.swift | 27 ++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 54f5035c..25e6d083 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -73,6 +73,7 @@ let package = Package( dependencies: [ .target(name: "AppCore"), .target(name: "ContactFeature"), + .target(name: "ContactsFeature"), .target(name: "HomeFeature"), .target(name: "RegisterFeature"), .target(name: "RestoreFeature"), @@ -133,6 +134,7 @@ let package = Package( name: "HomeFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "ContactsFeature"), .target(name: "RegisterFeature"), .target(name: "UserSearchFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 01e31519..2caa4f0f 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -1,5 +1,6 @@ import AppCore import ContactFeature +import ContactsFeature import Foundation import HomeFeature import RegisterFeature @@ -48,6 +49,9 @@ extension AppEnvironment { bgQueue: bgQueue ) }, + contacts: { + ContactsEnvironment() + }, userSearch: { UserSearchEnvironment( messenger: messenger, diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 9df6d745..faa5757c 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -2,6 +2,7 @@ import AppCore import Combine import ComposableArchitecture import ComposablePresentation +import ContactsFeature import Foundation import RegisterFeature import UserSearchFeature @@ -16,6 +17,7 @@ public struct HomeState: Equatable { isDeletingAccount: Bool = false, alert: AlertState<HomeAction>? = nil, register: RegisterState? = nil, + contacts: ContactsState? = nil, userSearch: UserSearchState? = nil ) { self.failure = failure @@ -23,6 +25,7 @@ public struct HomeState: Equatable { self.isDeletingAccount = isDeletingAccount self.alert = alert self.register = register + self.contacts = contacts self.userSearch = userSearch } @@ -32,6 +35,7 @@ public struct HomeState: Equatable { public var isDeletingAccount: Bool public var alert: AlertState<HomeAction>? public var register: RegisterState? + public var contacts: ContactsState? public var userSearch: UserSearchState? } @@ -64,7 +68,10 @@ public enum HomeAction: Equatable { case didDismissRegister case userSearchButtonTapped case didDismissUserSearch + case contactsButtonTapped + case didDismissContacts case register(RegisterAction) + case contacts(ContactsAction) case userSearch(UserSearchAction) } @@ -75,6 +82,7 @@ public struct HomeEnvironment { mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, + contacts: @escaping () -> ContactsEnvironment, userSearch: @escaping () -> UserSearchEnvironment ) { self.messenger = messenger @@ -82,6 +90,7 @@ public struct HomeEnvironment { self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register + self.contacts = contacts self.userSearch = userSearch } @@ -90,6 +99,7 @@ public struct HomeEnvironment { public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment + public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment } @@ -100,6 +110,7 @@ extension HomeEnvironment { mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, + contacts: { .unimplemented }, userSearch: { .unimplemented } ) } @@ -238,11 +249,19 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.userSearch = nil return .none + case .contactsButtonTapped: + state.contacts = ContactsState() + return .none + + case .didDismissContacts: + state.contacts = nil + return .none + case .register(.finished): state.register = nil return Effect(value: .messenger(.start)) - case .register(_), .userSearch(_): + case .register(_), .contacts(_), .userSearch(_): return .none } } @@ -253,6 +272,13 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> action: /HomeAction.register, environment: { $0.register() } ) +.presenting( + contactsReducer, + state: .keyPath(\.contacts), + id: .notNil(), + action: /HomeAction.contacts, + environment: { $0.contacts() } +) .presenting( userSearchReducer, state: .keyPath(\.userSearch), diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index e52e8d1b..414bf928 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import ComposablePresentation +import ContactsFeature import RegisterFeature import SwiftUI import UserSearchFeature @@ -88,6 +89,16 @@ public struct HomeView: View { } Section { + Button { + viewStore.send(.contactsButtonTapped) + } label: { + HStack { + Text("Contacts") + Spacer() + Image(systemName: "chevron.forward") + } + } + Button { viewStore.send(.userSearchButtonTapped) } label: { @@ -123,6 +134,16 @@ public struct HomeView: View { store.scope(state: \.alert), dismiss: HomeAction.didDismissAlert ) + .background(NavigationLinkWithStore( + store.scope( + state: \.contacts, + action: HomeAction.contacts + ), + onDeactivate: { + viewStore.send(.didDismissContacts) + }, + destination: ContactsView.init(store:) + )) .background(NavigationLinkWithStore( store.scope( state: \.userSearch, diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 2d7f6daf..89aef4c9 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import ContactsFeature import RegisterFeature import UserSearchFeature import XCTest @@ -464,4 +465,30 @@ final class HomeFeatureTests: XCTestCase { $0.userSearch = nil } } + + func testContactsButtonTapped() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + store.send(.contactsButtonTapped) { + $0.contacts = ContactsState() + } + } + + func testDidDismissContacts() { + let store = TestStore( + initialState: HomeState( + contacts: ContactsState() + ), + reducer: homeReducer, + environment: .unimplemented + ) + + store.send(.didDismissContacts) { + $0.contacts = nil + } + } } -- GitLab From 300a762ac5ae9885e7969ed3287798547f949c32 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 13:36:06 +0200 Subject: [PATCH 3/5] Fetch db contacts --- .../AppFeature/AppEnvironment+Live.swift | 6 ++- .../ContactsFeature/ContactsFeature.swift | 43 ++++++++++++++-- .../ContactsFeature/ContactsView.swift | 49 +++++++++++++++++-- .../ContactsFeatureTests.swift | 32 ++++++++++++ 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 2caa4f0f..8ac88751 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -50,7 +50,11 @@ extension AppEnvironment { ) }, contacts: { - ContactsEnvironment() + ContactsEnvironment( + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) }, userSearch: { UserSearchEnvironment( diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift index e7f98fe7..c5780594 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift @@ -1,21 +1,47 @@ +import AppCore import ComposableArchitecture +import Foundation import XCTestDynamicOverlay +import XXModels public struct ContactsState: Equatable { - public init() {} + public init( + contacts: IdentifiedArrayOf<Contact> = [] + ) { + self.contacts = contacts + } + + public var contacts: IdentifiedArrayOf<XXModels.Contact> } public enum ContactsAction: Equatable { case start + case didFetchContacts([XXModels.Contact]) } public struct ContactsEnvironment { - public init() {} + public init( + db: DBManagerGetDB, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.db = db + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var db: DBManagerGetDB + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension ContactsEnvironment { - public static let unimplemented = ContactsEnvironment() + public static let unimplemented = ContactsEnvironment( + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif @@ -23,6 +49,17 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi { state, action, env in switch action { case .start: + return Effect + .catching { try env.db() } + .flatMap { $0.fetchContactsPublisher(.init()) } + .assertNoFailure() + .map(ContactsAction.didFetchContacts) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didFetchContacts(let contacts): + state.contacts = IdentifiedArray(uniqueElements: contacts) return .none } } diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift index 870889f2..ad5d1a37 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -1,5 +1,7 @@ +import AppCore import ComposableArchitecture import SwiftUI +import XXModels public struct ContactsView: View { public init(store: Store<ContactsState, ContactsAction>) { @@ -9,13 +11,36 @@ public struct ContactsView: View { let store: Store<ContactsState, ContactsAction> struct ViewState: Equatable { - init(state: ContactsState) {} + var contacts: IdentifiedArrayOf<XXModels.Contact> + + init(state: ContactsState) { + contacts = state.contacts + } } public var body: some View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { - + ForEach(viewStore.contacts) { contact in + Section { + Button { + // TODO: + } label: { + HStack { + VStack(alignment: .leading, spacing: 8) { + Label(contact.username ?? "", systemImage: "person") + Label(contact.email ?? "", systemImage: "envelope") + Label(contact.phone ?? "", systemImage: "phone") + } + .font(.callout) + .tint(Color.primary) + Spacer() + Image(systemName: "chevron.forward") + } + } + ContactAuthStatusView(contact.authStatus) + } + } } .navigationTitle("Contacts") .task { viewStore.send(.start) } @@ -28,7 +53,25 @@ public struct ContactsView_Previews: PreviewProvider { public static var previews: some View { NavigationView { ContactsView(store: Store( - initialState: ContactsState(), + initialState: ContactsState( + contacts: [ + .init( + id: "1".data(using: .utf8)!, + username: "John Doe", + email: "john@doe.com", + phone: "+1234567890", + authStatus: .friend + ), + .init( + id: "2".data(using: .utf8)!, + username: "Alice Unknown", + authStatus: .requested + ), + .init( + id: "3".data(using: .utf8)! + ), + ] + ), reducer: .empty, environment: () )) diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift index 3b521768..04533b34 100644 --- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift @@ -1,5 +1,8 @@ +import Combine import ComposableArchitecture +import CustomDump import XCTest +import XXModels @testable import ContactsFeature final class ContactsFeatureTests: XCTestCase { @@ -10,6 +13,35 @@ final class ContactsFeatureTests: XCTestCase { environment: .unimplemented ) + var didFetchContacts: [XXModels.Contact.Query] = [] + let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.db.run = { + var db: Database = .failing + db.fetchContactsPublisher.run = { query in + didFetchContacts.append(query) + return contactsPublisher.eraseToAnyPublisher() + } + return db + } + store.send(.start) + + XCTAssertNoDifference(didFetchContacts, [XXModels.Contact.Query()]) + + let contacts: [XXModels.Contact] = [ + .init(id: "1".data(using: .utf8)!), + .init(id: "2".data(using: .utf8)!), + .init(id: "3".data(using: .utf8)!), + ] + contactsPublisher.send(contacts) + + store.receive(.didFetchContacts(contacts)) { + $0.contacts = IdentifiedArray(uniqueElements: contacts) + } + + contactsPublisher.send(completion: .finished) } } -- GitLab From d0f0e6e06c8bc1182c07ed2b76f5043c9e52577e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 13:46:46 +0200 Subject: [PATCH 4/5] Present Contact from Contacts --- Examples/xx-messenger/Package.swift | 1 + .../AppFeature/AppEnvironment+Live.swift | 35 +++++++++--------- .../ContactsFeature/ContactsFeature.swift | 36 +++++++++++++++++-- .../ContactsFeature/ContactsView.swift | 12 ++++++- .../ContactsFeatureTests.swift | 32 +++++++++++++++++ 5 files changed, 95 insertions(+), 21 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 25e6d083..db3097ee 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -117,6 +117,7 @@ let package = Package( name: "ContactsFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "ContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXModels", package: "client-ios-db"), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 8ac88751..f57003f0 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -19,6 +19,21 @@ extension AppEnvironment { let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() + let contactEnvironment = ContactEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue, + sendRequest: { + SendRequestEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + } + ) + return AppEnvironment( dbManager: dbManager, messenger: messenger, @@ -53,7 +68,8 @@ extension AppEnvironment { ContactsEnvironment( db: dbManager.getDB, mainQueue: mainQueue, - bgQueue: bgQueue + bgQueue: bgQueue, + contact: { contactEnvironment } ) }, userSearch: { @@ -61,22 +77,7 @@ extension AppEnvironment { messenger: messenger, mainQueue: mainQueue, bgQueue: bgQueue, - contact: { - ContactEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue, - sendRequest: { - SendRequestEnvironment( - messenger: messenger, - db: dbManager.getDB, - mainQueue: mainQueue, - bgQueue: bgQueue - ) - } - ) - } + contact: { contactEnvironment } ) } ) diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift index c5780594..79690934 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift @@ -1,38 +1,49 @@ import AppCore import ComposableArchitecture +import ComposablePresentation +import ContactFeature import Foundation import XCTestDynamicOverlay import XXModels public struct ContactsState: Equatable { public init( - contacts: IdentifiedArrayOf<Contact> = [] + contacts: IdentifiedArrayOf<Contact> = [], + contact: ContactState? = nil ) { self.contacts = contacts + self.contact = contact } public var contacts: IdentifiedArrayOf<XXModels.Contact> + public var contact: ContactState? } public enum ContactsAction: Equatable { case start case didFetchContacts([XXModels.Contact]) + case contactSelected(XXModels.Contact) + case contactDismissed + case contact(ContactAction) } public struct ContactsEnvironment { public init( db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, - bgQueue: AnySchedulerOf<DispatchQueue> + bgQueue: AnySchedulerOf<DispatchQueue>, + contact: @escaping () -> ContactEnvironment ) { self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue + self.contact = contact } public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var contact: () -> ContactEnvironment } #if DEBUG @@ -40,7 +51,8 @@ extension ContactsEnvironment { public static let unimplemented = ContactsEnvironment( db: .unimplemented, mainQueue: .unimplemented, - bgQueue: .unimplemented + bgQueue: .unimplemented, + contact: { .unimplemented } ) } #endif @@ -61,5 +73,23 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi case .didFetchContacts(let contacts): state.contacts = IdentifiedArray(uniqueElements: contacts) return .none + + case .contactSelected(let contact): + state.contact = ContactState(id: contact.id, dbContact: contact) + return .none + + case .contactDismissed: + state.contact = nil + return .none + + case .contact(_): + return .none } } +.presenting( + contactReducer, + state: .keyPath(\.contact), + id: .keyPath(\.?.id), + action: /ContactsAction.contact, + environment: { $0.contact() } +) diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift index ad5d1a37..7b02efd1 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -1,5 +1,7 @@ import AppCore import ComposableArchitecture +import ComposablePresentation +import ContactFeature import SwiftUI import XXModels @@ -24,7 +26,7 @@ public struct ContactsView: View { ForEach(viewStore.contacts) { contact in Section { Button { - // TODO: + viewStore.send(.contactSelected(contact)) } label: { HStack { VStack(alignment: .leading, spacing: 8) { @@ -44,6 +46,14 @@ public struct ContactsView: View { } .navigationTitle("Contacts") .task { viewStore.send(.start) } + .background(NavigationLinkWithStore( + store.scope( + state: \.contact, + action: ContactsAction.contact + ), + onDeactivate: { viewStore.send(.contactDismissed) }, + destination: ContactView.init(store:) + )) } } } diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift index 04533b34..6c68f406 100644 --- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift @@ -1,5 +1,6 @@ import Combine import ComposableArchitecture +import ContactFeature import CustomDump import XCTest import XXModels @@ -44,4 +45,35 @@ final class ContactsFeatureTests: XCTestCase { contactsPublisher.send(completion: .finished) } + + func testSelectContact() { + let store = TestStore( + initialState: ContactsState(), + reducer: contactsReducer, + environment: .unimplemented + ) + + let contact = XXModels.Contact(id: "id".data(using: .utf8)!) + + store.send(.contactSelected(contact)) { + $0.contact = ContactState(id: contact.id, dbContact: contact) + } + } + + func testDismissContact() { + let store = TestStore( + initialState: ContactsState( + contact: ContactState( + id: "id".data(using: .utf8)!, + dbContact: Contact(id: "id".data(using: .utf8)!) + ) + ), + reducer: contactsReducer, + environment: .unimplemented + ) + + store.send(.contactDismissed) { + $0.contact = nil + } + } } -- GitLab From 844924bdcba1503cd0fba77e7a04cd765132129c Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 8 Sep 2022 14:03:17 +0200 Subject: [PATCH 5/5] Highlight my contact on the list --- Examples/xx-messenger/Package.swift | 2 + .../AppFeature/AppEnvironment+Live.swift | 1 + .../ContactsFeature/ContactsFeature.swift | 18 +++++++- .../ContactsFeature/ContactsView.swift | 44 +++++++++++++------ .../ContactsFeatureTests.swift | 22 +++++++++- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index db3097ee..5df8eb39 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -120,6 +120,8 @@ let package = Package( .target(name: "ContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index f57003f0..61bfba1f 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -66,6 +66,7 @@ extension AppEnvironment { }, contacts: { ContactsEnvironment( + messenger: messenger, db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift index 79690934..1ded89de 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift @@ -4,17 +4,22 @@ import ComposablePresentation import ContactFeature import Foundation import XCTestDynamicOverlay +import XXClient +import XXMessengerClient import XXModels public struct ContactsState: Equatable { public init( - contacts: IdentifiedArrayOf<Contact> = [], + myId: Data? = nil, + contacts: IdentifiedArrayOf<XXModels.Contact> = [], contact: ContactState? = nil ) { + self.myId = myId self.contacts = contacts self.contact = contact } + public var myId: Data? public var contacts: IdentifiedArrayOf<XXModels.Contact> public var contact: ContactState? } @@ -29,17 +34,20 @@ public enum ContactsAction: Equatable { public struct ContactsEnvironment { public init( + messenger: Messenger, db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, contact: @escaping () -> ContactEnvironment ) { + self.messenger = messenger self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue self.contact = contact } + public var messenger: Messenger public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> @@ -49,6 +57,7 @@ public struct ContactsEnvironment { #if DEBUG extension ContactsEnvironment { public static let unimplemented = ContactsEnvironment( + messenger: .unimplemented, db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, @@ -61,6 +70,7 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi { state, action, env in switch action { case .start: + state.myId = try? env.messenger.e2e.tryGet().getContact().getId() return Effect .catching { try env.db() } .flatMap { $0.fetchContactsPublisher(.init()) } @@ -70,7 +80,11 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi .receive(on: env.mainQueue) .eraseToEffect() - case .didFetchContacts(let contacts): + case .didFetchContacts(var contacts): + if let myId = state.myId, + let myIndex = contacts.firstIndex(where: { $0.id == myId }) { + contacts.move(fromOffsets: [myIndex], toOffset: contacts.startIndex) + } state.contacts = IdentifiedArray(uniqueElements: contacts) return .none diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift index 7b02efd1..b7798685 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -13,9 +13,11 @@ public struct ContactsView: View { let store: Store<ContactsState, ContactsAction> struct ViewState: Equatable { + var myId: Data? var contacts: IdentifiedArrayOf<XXModels.Contact> init(state: ContactsState) { + myId = state.myId contacts = state.contacts } } @@ -24,23 +26,37 @@ public struct ContactsView: View { WithViewStore(store.scope(state: ViewState.init)) { viewStore in Form { ForEach(viewStore.contacts) { contact in - Section { - Button { - viewStore.send(.contactSelected(contact)) - } label: { - HStack { - VStack(alignment: .leading, spacing: 8) { - Label(contact.username ?? "", systemImage: "person") - Label(contact.email ?? "", systemImage: "envelope") - Label(contact.phone ?? "", systemImage: "phone") + if contact.id == viewStore.myId { + Section { + VStack(alignment: .leading, spacing: 8) { + Label(contact.username ?? "", systemImage: "person") + Label(contact.email ?? "", systemImage: "envelope") + Label(contact.phone ?? "", systemImage: "phone") + } + .font(.callout) + .tint(Color.primary) + } header: { + Text("My contact") + } + } else { + Section { + Button { + viewStore.send(.contactSelected(contact)) + } label: { + HStack { + VStack(alignment: .leading, spacing: 8) { + Label(contact.username ?? "", systemImage: "person") + Label(contact.email ?? "", systemImage: "envelope") + Label(contact.phone ?? "", systemImage: "phone") + } + .font(.callout) + .tint(Color.primary) + Spacer() + Image(systemName: "chevron.forward") } - .font(.callout) - .tint(Color.primary) - Spacer() - Image(systemName: "chevron.forward") } + ContactAuthStatusView(contact.authStatus) } - ContactAuthStatusView(contact.authStatus) } } } diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift index 6c68f406..a0c0291e 100644 --- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift @@ -3,6 +3,8 @@ import ComposableArchitecture import ContactFeature import CustomDump import XCTest +import XXClient +import XXMessengerClient import XXModels @testable import ContactsFeature @@ -14,11 +16,21 @@ final class ContactsFeatureTests: XCTestCase { environment: .unimplemented ) + let myId = "2".data(using: .utf8)! var didFetchContacts: [XXModels.Contact.Query] = [] let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in myId } + return contact + } + return e2e + } store.environment.db.run = { var db: Database = .failing db.fetchContactsPublisher.run = { query in @@ -28,7 +40,9 @@ final class ContactsFeatureTests: XCTestCase { return db } - store.send(.start) + store.send(.start) { + $0.myId = myId + } XCTAssertNoDifference(didFetchContacts, [XXModels.Contact.Query()]) @@ -40,7 +54,11 @@ final class ContactsFeatureTests: XCTestCase { contactsPublisher.send(contacts) store.receive(.didFetchContacts(contacts)) { - $0.contacts = IdentifiedArray(uniqueElements: contacts) + $0.contacts = IdentifiedArray(uniqueElements: [ + contacts[1], + contacts[0], + contacts[2], + ]) } contactsPublisher.send(completion: .finished) -- GitLab