diff --git a/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift b/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift index ae533463a649f3c5cf6e72be53d6462ed0a21f66..9d12a95d49af444dc165f3b92ddbc6838ffb0ca0 100644 --- a/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift +++ b/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift @@ -1,20 +1,49 @@ +import AppCore import ComposableArchitecture +import Foundation +import XXModels public struct GroupsComponent: ReducerProtocol { public struct State: Equatable { - public init() {} + public init( + groups: IdentifiedArrayOf<Group> = [] + ) { + self.groups = groups + } + + public var groups: IdentifiedArrayOf<XXModels.Group> = [] } public enum Action: Equatable { case start + case didFetchGroups([XXModels.Group]) + case didSelectGroup(XXModels.Group) } public init() {} + @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB + @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue> + @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue> + public var body: some ReducerProtocol<State, Action> { Reduce { state, action in switch action { case .start: + return Effect + .catching { try db() } + .flatMap { $0.fetchGroupsPublisher.callAsFunction(.init()) } + .assertNoFailure() + .map(Action.didFetchGroups) + .subscribe(on: bgQueue) + .receive(on: mainQueue) + .eraseToEffect() + + case .didFetchGroups(let groups): + state.groups = IdentifiedArray(uniqueElements: groups) + return .none + + case .didSelectGroup(_): return .none } } diff --git a/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift b/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift index 38cbefbe59da5e22b68830415e89ad7f3724c519..79e102148377c3f30234df5ff32e9fd021f5d2ce 100644 --- a/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift +++ b/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift @@ -1,5 +1,7 @@ +import AppCore import ComposableArchitecture import SwiftUI +import XXModels public struct GroupsView: View { public typealias Component = GroupsComponent @@ -12,17 +14,43 @@ public struct GroupsView: View { struct ViewState: Equatable { init(state: Component.State) {} + + var groups: IdentifiedArrayOf<XXModels.Group> = [] } public var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { - + ForEach(viewStore.groups) { group in + groupView(group) { + viewStore.send(.didSelectGroup(group)) + } + } } .navigationTitle("Groups") .task { viewStore.send(.start) } } } + + func groupView( + _ group: XXModels.Group, + onSelect: @escaping () -> Void + ) -> some View { + Section { + Button { + onSelect() + } label: { + HStack { + Label(group.name, systemImage: "person.3") + .font(.callout) + .tint(Color.primary) + Spacer() + Image(systemName: "chevron.forward") + } + GroupAuthStatusView(group.authStatus) + } + } + } } #if DEBUG diff --git a/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift b/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift index be3456a9494aa0d1dc5677e27432f33516babcc8..c726955f4cc892ecffda8694a3a5bec90c54544d 100644 --- a/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift +++ b/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift @@ -1,14 +1,89 @@ +import Combine import ComposableArchitecture +import CustomDump import XCTest +import XXModels @testable import GroupsFeature final class GroupsComponentTests: XCTestCase { + enum Action: Equatable { + case didFetchGroups(XXModels.Group.Query) + } + + var actions: [Action]! + + override func setUp() { + actions = [] + } + + override func tearDown() { + actions = nil + } + func testStart() { + let groupsSubject = PassthroughSubject<[XXModels.Group], Error>() + let store = TestStore( initialState: GroupsComponent.State(), reducer: GroupsComponent() ) + store.dependencies.app.mainQueue = .immediate + store.dependencies.app.bgQueue = .immediate + store.dependencies.app.dbManager.getDB.run = { + var db: Database = .unimplemented + db.fetchGroupsPublisher.run = { query in + self.actions.append(.didFetchGroups(query)) + return groupsSubject.eraseToAnyPublisher() + } + return db + } + store.send(.start) + + XCTAssertNoDifference(actions, [ + .didFetchGroups(.init()) + ]) + + let groups: [XXModels.Group] = [ + .stub(1), + .stub(2), + .stub(3), + ] + groupsSubject.send(groups) + + store.receive(.didFetchGroups(groups)) { + $0.groups = IdentifiedArray(uniqueElements: groups) + } + + groupsSubject.send(completion: .finished) + } + + func testSelectGroup() { + let store = TestStore( + initialState: GroupsComponent.State( + groups: IdentifiedArray(uniqueElements: [ + .stub(1), + .stub(2), + .stub(3), + ]) + ), + reducer: GroupsComponent() + ) + + store.send(.didSelectGroup(.stub(2))) + } +} + +private extension XXModels.Group { + static func stub(_ id: Int) -> XXModels.Group { + XXModels.Group( + id: "group-\(id)-id".data(using: .utf8)!, + name: "Group \(id)", + leaderId: "group-\(id)-leader-id".data(using: .utf8)!, + createdAt: Date(timeIntervalSince1970: TimeInterval(id * 86_400)), + authStatus: .participating, + serialized: "group-\(id)-serialized".data(using: .utf8)! + ) } }