Skip to content
Snippets Groups Projects
Commit 59b4b115 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Migrate ContactsFeature to ReducerProtocol

parent f6df3d2f
No related branches found
No related tags found
2 merge requests!126Migrate example app to ComposableArchitecture's ReducerProtocol,!102Release 1.0.0
import AppCore
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import Foundation
import MyContactFeature
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct ContactsComponent: ReducerProtocol {
public struct State: Equatable {
public init(
myId: Data? = nil,
contacts: IdentifiedArrayOf<XXModels.Contact> = [],
contact: ContactComponent.State? = nil,
myContact: MyContactComponent.State? = nil
) {
self.myId = myId
self.contacts = contacts
self.contact = contact
self.myContact = myContact
}
public var myId: Data?
public var contacts: IdentifiedArrayOf<XXModels.Contact>
public var contact: ContactComponent.State?
public var myContact: MyContactComponent.State?
}
public enum Action: Equatable {
case start
case didFetchContacts([XXModels.Contact])
case contactSelected(XXModels.Contact)
case contactDismissed
case contact(ContactComponent.Action)
case myContactSelected
case myContactDismissed
case myContact(MyContactComponent.Action)
}
public init() {}
@Dependency(\.app.messenger) var messenger: Messenger
@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:
state.myId = try? messenger.e2e.tryGet().getContact().getId()
return Effect
.catching { try db() }
.flatMap { $0.fetchContactsPublisher(.init()) }
.assertNoFailure()
.map(Action.didFetchContacts)
.subscribe(on: bgQueue)
.receive(on: mainQueue)
.eraseToEffect()
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
case .contactSelected(let contact):
state.contact = ContactComponent.State(id: contact.id, dbContact: contact)
return .none
case .contactDismissed:
state.contact = nil
return .none
case .myContactSelected:
state.myContact = MyContactComponent.State()
return .none
case .myContactDismissed:
state.myContact = nil
return .none
case .contact(_), .myContact(_):
return .none
}
}
.presenting(
state: .keyPath(\.contact),
id: .keyPath(\.?.id),
action: /Action.contact,
presented: { ContactComponent() }
)
.presenting(
state: .keyPath(\.myContact),
id: .notNil(),
action: /Action.myContact,
presented: { MyContactComponent() }
)
}
}
import AppCore
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import Foundation
import MyContactFeature
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct ContactsState: Equatable {
public init(
myId: Data? = nil,
contacts: IdentifiedArrayOf<XXModels.Contact> = [],
contact: ContactState? = nil,
myContact: MyContactState? = nil
) {
self.myId = myId
self.contacts = contacts
self.contact = contact
self.myContact = myContact
}
public var myId: Data?
public var contacts: IdentifiedArrayOf<XXModels.Contact>
public var contact: ContactState?
public var myContact: MyContactState?
}
public enum ContactsAction: Equatable {
case start
case didFetchContacts([XXModels.Contact])
case contactSelected(XXModels.Contact)
case contactDismissed
case contact(ContactAction)
case myContactSelected
case myContactDismissed
case myContact(MyContactAction)
}
public struct ContactsEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
contact: @escaping () -> ContactEnvironment,
myContact: @escaping () -> MyContactEnvironment
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.contact = contact
self.myContact = myContact
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var contact: () -> ContactEnvironment
public var myContact: () -> MyContactEnvironment
}
#if DEBUG
extension ContactsEnvironment {
public static let unimplemented = ContactsEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
contact: { .unimplemented },
myContact: { .unimplemented }
)
}
#endif
public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvironment>
{ 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()) }
.assertNoFailure()
.map(ContactsAction.didFetchContacts)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
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
case .contactSelected(let contact):
state.contact = ContactState(id: contact.id, dbContact: contact)
return .none
case .contactDismissed:
state.contact = nil
return .none
case .myContactSelected:
state.myContact = MyContactState()
return .none
case .myContactDismissed:
state.myContact = nil
return .none
case .contact(_), .myContact(_):
return .none
}
}
.presenting(
contactReducer,
state: .keyPath(\.contact),
id: .keyPath(\.?.id),
action: /ContactsAction.contact,
environment: { $0.contact() }
)
.presenting(
myContactReducer,
state: .keyPath(\.myContact),
id: .notNil(),
action: /ContactsAction.myContact,
environment: { $0.myContact() }
)
...@@ -7,17 +7,17 @@ import SwiftUI ...@@ -7,17 +7,17 @@ import SwiftUI
import XXModels import XXModels
public struct ContactsView: View { public struct ContactsView: View {
public init(store: Store<ContactsState, ContactsAction>) { public init(store: StoreOf<ContactsComponent>) {
self.store = store self.store = store
} }
let store: Store<ContactsState, ContactsAction> let store: StoreOf<ContactsComponent>
struct ViewState: Equatable { struct ViewState: Equatable {
var myId: Data? var myId: Data?
var contacts: IdentifiedArrayOf<XXModels.Contact> var contacts: IdentifiedArrayOf<XXModels.Contact>
init(state: ContactsState) { init(state: ContactsComponent.State) {
myId = state.myId myId = state.myId
contacts = state.contacts contacts = state.contacts
} }
...@@ -74,7 +74,7 @@ public struct ContactsView: View { ...@@ -74,7 +74,7 @@ public struct ContactsView: View {
.background(NavigationLinkWithStore( .background(NavigationLinkWithStore(
store.scope( store.scope(
state: \.contact, state: \.contact,
action: ContactsAction.contact action: ContactsComponent.Action.contact
), ),
onDeactivate: { viewStore.send(.contactDismissed) }, onDeactivate: { viewStore.send(.contactDismissed) },
destination: ContactView.init(store:) destination: ContactView.init(store:)
...@@ -82,7 +82,7 @@ public struct ContactsView: View { ...@@ -82,7 +82,7 @@ public struct ContactsView: View {
.background(NavigationLinkWithStore( .background(NavigationLinkWithStore(
store.scope( store.scope(
state: \.myContact, state: \.myContact,
action: ContactsAction.myContact action: ContactsComponent.Action.myContact
), ),
onDeactivate: { viewStore.send(.myContactDismissed) }, onDeactivate: { viewStore.send(.myContactDismissed) },
destination: MyContactView.init(store:) destination: MyContactView.init(store:)
...@@ -96,7 +96,7 @@ public struct ContactsView_Previews: PreviewProvider { ...@@ -96,7 +96,7 @@ public struct ContactsView_Previews: PreviewProvider {
public static var previews: some View { public static var previews: some View {
NavigationView { NavigationView {
ContactsView(store: Store( ContactsView(store: Store(
initialState: ContactsState( initialState: ContactsComponent.State(
contacts: [ contacts: [
.init( .init(
id: "1".data(using: .utf8)!, id: "1".data(using: .utf8)!,
...@@ -115,8 +115,7 @@ public struct ContactsView_Previews: PreviewProvider { ...@@ -115,8 +115,7 @@ public struct ContactsView_Previews: PreviewProvider {
), ),
] ]
), ),
reducer: .empty, reducer: EmptyReducer()
environment: ()
)) ))
} }
} }
......
...@@ -9,21 +9,20 @@ import XXMessengerClient ...@@ -9,21 +9,20 @@ import XXMessengerClient
import XXModels import XXModels
@testable import ContactsFeature @testable import ContactsFeature
final class ContactsFeatureTests: XCTestCase { final class ContactsComponentTests: XCTestCase {
func testStart() { func testStart() {
let store = TestStore( let store = TestStore(
initialState: ContactsState(), initialState: ContactsComponent.State(),
reducer: contactsReducer, reducer: ContactsComponent()
environment: .unimplemented
) )
let myId = "2".data(using: .utf8)! let myId = "2".data(using: .utf8)!
var didFetchContacts: [XXModels.Contact.Query] = [] var didFetchContacts: [XXModels.Contact.Query] = []
let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
store.environment.mainQueue = .immediate store.dependencies.app.mainQueue = .immediate
store.environment.bgQueue = .immediate store.dependencies.app.bgQueue = .immediate
store.environment.messenger.e2e.get = { store.dependencies.app.messenger.e2e.get = {
var e2e: E2E = .unimplemented var e2e: E2E = .unimplemented
e2e.getContact.run = { e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data()) var contact: XXClient.Contact = .unimplemented(Data())
...@@ -32,7 +31,7 @@ final class ContactsFeatureTests: XCTestCase { ...@@ -32,7 +31,7 @@ final class ContactsFeatureTests: XCTestCase {
} }
return e2e return e2e
} }
store.environment.db.run = { store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented var db: Database = .unimplemented
db.fetchContactsPublisher.run = { query in db.fetchContactsPublisher.run = { query in
didFetchContacts.append(query) didFetchContacts.append(query)
...@@ -67,28 +66,26 @@ final class ContactsFeatureTests: XCTestCase { ...@@ -67,28 +66,26 @@ final class ContactsFeatureTests: XCTestCase {
func testSelectContact() { func testSelectContact() {
let store = TestStore( let store = TestStore(
initialState: ContactsState(), initialState: ContactsComponent.State(),
reducer: contactsReducer, reducer: ContactsComponent()
environment: .unimplemented
) )
let contact = XXModels.Contact(id: "id".data(using: .utf8)!) let contact = XXModels.Contact(id: "id".data(using: .utf8)!)
store.send(.contactSelected(contact)) { store.send(.contactSelected(contact)) {
$0.contact = ContactState(id: contact.id, dbContact: contact) $0.contact = ContactComponent.State(id: contact.id, dbContact: contact)
} }
} }
func testDismissContact() { func testDismissContact() {
let store = TestStore( let store = TestStore(
initialState: ContactsState( initialState: ContactsComponent.State(
contact: ContactState( contact: ContactComponent.State(
id: "id".data(using: .utf8)!, id: "id".data(using: .utf8)!,
dbContact: Contact(id: "id".data(using: .utf8)!) dbContact: Contact(id: "id".data(using: .utf8)!)
) )
), ),
reducer: contactsReducer, reducer: ContactsComponent()
environment: .unimplemented
) )
store.send(.contactDismissed) { store.send(.contactDismissed) {
...@@ -98,23 +95,21 @@ final class ContactsFeatureTests: XCTestCase { ...@@ -98,23 +95,21 @@ final class ContactsFeatureTests: XCTestCase {
func testSelectMyContact() { func testSelectMyContact() {
let store = TestStore( let store = TestStore(
initialState: ContactsState(), initialState: ContactsComponent.State(),
reducer: contactsReducer, reducer: ContactsComponent()
environment: .unimplemented
) )
store.send(.myContactSelected) { store.send(.myContactSelected) {
$0.myContact = MyContactState() $0.myContact = MyContactComponent.State()
} }
} }
func testDismissMyContact() { func testDismissMyContact() {
let store = TestStore( let store = TestStore(
initialState: ContactsState( initialState: ContactsComponent.State(
myContact: MyContactState() myContact: MyContactComponent.State()
), ),
reducer: contactsReducer, reducer: ContactsComponent()
environment: .unimplemented
) )
store.send(.myContactDismissed) { store.send(.myContactDismissed) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment