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

Migrate RegisterFeature to ReducerProtocol

parent d58985d4
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 Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct RegisterComponent: ReducerProtocol {
public struct State: Equatable {
public enum Error: Swift.Error, Equatable {
case usernameMismatch(registering: String, registered: String?)
}
public enum Field: String, Hashable {
case username
}
public init(
focusedField: Field? = nil,
username: String = "",
isRegistering: Bool = false,
failure: String? = nil
) {
self.focusedField = focusedField
self.username = username
self.isRegistering = isRegistering
self.failure = failure
}
@BindableState public var focusedField: Field?
@BindableState public var username: String
public var isRegistering: Bool
public var failure: String?
}
public enum Action: Equatable, BindableAction {
case registerTapped
case failed(String)
case finished
case binding(BindingAction<State>)
}
public init() {}
@Dependency(\.app.messenger) var messenger: Messenger
@Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
@Dependency(\.app.now) var now: () -> Date
@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 .binding(_):
return .none
case .registerTapped:
state.focusedField = nil
state.isRegistering = true
state.failure = nil
return .future { [username = state.username] fulfill in
do {
let db = try db()
try messenger.register(username: username)
let contact = try messenger.myContact()
let facts = try contact.getFacts()
try db.saveContact(Contact(
id: try contact.getId(),
marshaled: contact.data,
username: facts.get(.username)?.value,
email: facts.get(.email)?.value,
phone: facts.get(.phone)?.value,
createdAt: now()
))
guard facts.get(.username)?.value == username else {
throw State.Error.usernameMismatch(
registering: username,
registered: facts.get(.username)?.value
)
}
fulfill(.success(.finished))
}
catch {
fulfill(.success(.failed(error.localizedDescription)))
}
}
.subscribe(on: bgQueue)
.receive(on: mainQueue)
.eraseToEffect()
case .failed(let failure):
state.isRegistering = false
state.failure = failure
return .none
case .finished:
state.isRegistering = false
return .none
}
}
}
}
import AppCore
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct RegisterState: Equatable {
public enum Error: Swift.Error, Equatable {
case usernameMismatch(registering: String, registered: String?)
}
public enum Field: String, Hashable {
case username
}
public init(
focusedField: Field? = nil,
username: String = "",
isRegistering: Bool = false,
failure: String? = nil
) {
self.focusedField = focusedField
self.username = username
self.isRegistering = isRegistering
self.failure = failure
}
@BindableState public var focusedField: Field?
@BindableState public var username: String
public var isRegistering: Bool
public var failure: String?
}
public enum RegisterAction: Equatable, BindableAction {
case registerTapped
case failed(String)
case finished
case binding(BindingAction<RegisterState>)
}
public struct RegisterEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
now: @escaping () -> Date,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>
) {
self.messenger = messenger
self.db = db
self.now = now
self.mainQueue = mainQueue
self.bgQueue = bgQueue
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var now: () -> Date
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
}
#if DEBUG
extension RegisterEnvironment {
public static let unimplemented = RegisterEnvironment(
messenger: .unimplemented,
db: .unimplemented,
now: XCTUnimplemented("\(Self.self).now"),
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
}
#endif
public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvironment>
{ state, action, env in
switch action {
case .binding(_):
return .none
case .registerTapped:
state.focusedField = nil
state.isRegistering = true
state.failure = nil
return .future { [username = state.username] fulfill in
do {
let db = try env.db()
try env.messenger.register(username: username)
let contact = try env.messenger.myContact()
let facts = try contact.getFacts()
try db.saveContact(Contact(
id: try contact.getId(),
marshaled: contact.data,
username: facts.get(.username)?.value,
email: facts.get(.email)?.value,
phone: facts.get(.phone)?.value,
createdAt: env.now()
))
guard facts.get(.username)?.value == username else {
throw RegisterState.Error.usernameMismatch(
registering: username,
registered: facts.get(.username)?.value
)
}
fulfill(.success(.finished))
}
catch {
fulfill(.success(.failed(error.localizedDescription)))
}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .failed(let failure):
state.isRegistering = false
state.failure = failure
return .none
case .finished:
state.isRegistering = false
return .none
}
}
.binding()
...@@ -2,22 +2,22 @@ import ComposableArchitecture ...@@ -2,22 +2,22 @@ import ComposableArchitecture
import SwiftUI import SwiftUI
public struct RegisterView: View { public struct RegisterView: View {
public init(store: Store<RegisterState, RegisterAction>) { public init(store: StoreOf<RegisterComponent>) {
self.store = store self.store = store
} }
let store: Store<RegisterState, RegisterAction> let store: StoreOf<RegisterComponent>
@FocusState var focusedField: RegisterState.Field? @FocusState var focusedField: RegisterComponent.State.Field?
struct ViewState: Equatable { struct ViewState: Equatable {
init(_ state: RegisterState) { init(_ state: RegisterComponent.State) {
focusedField = state.focusedField focusedField = state.focusedField
username = state.username username = state.username
isRegistering = state.isRegistering isRegistering = state.isRegistering
failure = state.failure failure = state.failure
} }
var focusedField: RegisterState.Field? var focusedField: RegisterComponent.State.Field?
var username: String var username: String
var isRegistering: Bool var isRegistering: Bool
var failure: String? var failure: String?
...@@ -31,7 +31,7 @@ public struct RegisterView: View { ...@@ -31,7 +31,7 @@ public struct RegisterView: View {
TextField( TextField(
text: viewStore.binding( text: viewStore.binding(
get: \.username, get: \.username,
send: { RegisterAction.set(\.$username, $0) } send: { RegisterComponent.Action.set(\.$username, $0) }
), ),
prompt: Text("Enter username"), prompt: Text("Enter username"),
label: { Text("Username") } label: { Text("Username") }
...@@ -78,9 +78,8 @@ public struct RegisterView: View { ...@@ -78,9 +78,8 @@ public struct RegisterView: View {
public struct RegisterView_Previews: PreviewProvider { public struct RegisterView_Previews: PreviewProvider {
public static var previews: some View { public static var previews: some View {
RegisterView(store: Store( RegisterView(store: Store(
initialState: RegisterState(), initialState: RegisterComponent.State(),
reducer: .empty, reducer: EmptyReducer()
environment: ()
)) ))
} }
} }
......
...@@ -6,7 +6,7 @@ import XXMessengerClient ...@@ -6,7 +6,7 @@ import XXMessengerClient
import XXModels import XXModels
@testable import RegisterFeature @testable import RegisterFeature
final class RegisterFeatureTests: XCTestCase { final class RegisterComponentTests: XCTestCase {
func testRegister() throws { func testRegister() throws {
let now = Date() let now = Date()
let username = "registering-username" let username = "registering-username"
...@@ -26,24 +26,23 @@ final class RegisterFeatureTests: XCTestCase { ...@@ -26,24 +26,23 @@ final class RegisterFeatureTests: XCTestCase {
var dbDidSaveContact: [XXModels.Contact] = [] var dbDidSaveContact: [XXModels.Contact] = []
let store = TestStore( let store = TestStore(
initialState: RegisterState(), initialState: RegisterComponent.State(),
reducer: registerReducer, reducer: RegisterComponent()
environment: .unimplemented
) )
store.environment.now = { now } store.dependencies.app.now = { now }
store.environment.mainQueue = .immediate store.dependencies.app.mainQueue = .immediate
store.environment.bgQueue = .immediate store.dependencies.app.bgQueue = .immediate
store.environment.messenger.register.run = { username in store.dependencies.app.messenger.register.run = { username in
messengerDidRegisterUsername.append(username) messengerDidRegisterUsername.append(username)
} }
store.environment.messenger.myContact.run = { includeFacts in store.dependencies.app.messenger.myContact.run = { includeFacts in
didGetMyContact.append(includeFacts) didGetMyContact.append(includeFacts)
var contact = XXClient.Contact.unimplemented(myContactData) var contact = XXClient.Contact.unimplemented(myContactData)
contact.getIdFromContact.run = { _ in myContactId } contact.getIdFromContact.run = { _ in myContactId }
contact.getFactsFromContact.run = { _ in myContactFacts } contact.getFactsFromContact.run = { _ in myContactFacts }
return contact return contact
} }
store.environment.db.run = { store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented var db: Database = .unimplemented
db.saveContact.run = { contact in db.saveContact.run = { contact in
dbDidSaveContact.append(contact) dbDidSaveContact.append(contact)
...@@ -87,17 +86,16 @@ final class RegisterFeatureTests: XCTestCase { ...@@ -87,17 +86,16 @@ final class RegisterFeatureTests: XCTestCase {
let error = Error() let error = Error()
let store = TestStore( let store = TestStore(
initialState: RegisterState(), initialState: RegisterComponent.State(),
reducer: registerReducer, reducer: RegisterComponent()
environment: .unimplemented
) )
let mainQueue = DispatchQueue.test let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test let bgQueue = DispatchQueue.test
store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler() store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.db.run = { throw error } store.dependencies.app.dbManager.getDB.run = { throw error }
store.send(.registerTapped) { store.send(.registerTapped) {
$0.isRegistering = true $0.isRegistering = true
...@@ -117,18 +115,17 @@ final class RegisterFeatureTests: XCTestCase { ...@@ -117,18 +115,17 @@ final class RegisterFeatureTests: XCTestCase {
let error = Error() let error = Error()
let store = TestStore( let store = TestStore(
initialState: RegisterState(), initialState: RegisterComponent.State(),
reducer: registerReducer, reducer: RegisterComponent()
environment: .unimplemented
) )
let mainQueue = DispatchQueue.test let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test let bgQueue = DispatchQueue.test
store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler() store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.db.run = { .unimplemented } store.dependencies.app.dbManager.getDB.run = { .unimplemented }
store.environment.messenger.register.run = { _ in throw error } store.dependencies.app.messenger.register.run = { _ in throw error }
store.send(.registerTapped) { store.send(.registerTapped) {
$0.isRegistering = true $0.isRegistering = true
...@@ -162,26 +159,25 @@ final class RegisterFeatureTests: XCTestCase { ...@@ -162,26 +159,25 @@ final class RegisterFeatureTests: XCTestCase {
var dbDidSaveContact: [XXModels.Contact] = [] var dbDidSaveContact: [XXModels.Contact] = []
let store = TestStore( let store = TestStore(
initialState: RegisterState( initialState: RegisterComponent.State(
username: username username: username
), ),
reducer: registerReducer, reducer: RegisterComponent()
environment: .unimplemented
) )
store.environment.now = { now } store.dependencies.app.now = { now }
store.environment.mainQueue = .immediate store.dependencies.app.mainQueue = .immediate
store.environment.bgQueue = .immediate store.dependencies.app.bgQueue = .immediate
store.environment.messenger.register.run = { username in store.dependencies.app.messenger.register.run = { username in
messengerDidRegisterUsername.append(username) messengerDidRegisterUsername.append(username)
} }
store.environment.messenger.myContact.run = { includeFacts in store.dependencies.app.messenger.myContact.run = { includeFacts in
didGetMyContact.append(includeFacts) didGetMyContact.append(includeFacts)
var contact = XXClient.Contact.unimplemented(myContactData) var contact = XXClient.Contact.unimplemented(myContactData)
contact.getIdFromContact.run = { _ in myContactId } contact.getIdFromContact.run = { _ in myContactId }
contact.getFactsFromContact.run = { _ in myContactFacts } contact.getFactsFromContact.run = { _ in myContactFacts }
return contact return contact
} }
store.environment.db.run = { store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented var db: Database = .unimplemented
db.saveContact.run = { contact in db.saveContact.run = { contact in
dbDidSaveContact.append(contact) dbDidSaveContact.append(contact)
...@@ -207,7 +203,7 @@ final class RegisterFeatureTests: XCTestCase { ...@@ -207,7 +203,7 @@ final class RegisterFeatureTests: XCTestCase {
) )
]) ])
let failure = RegisterState.Error.usernameMismatch( let failure = RegisterComponent.State.Error.usernameMismatch(
registering: username, registering: username,
registered: myContactUsername registered: myContactUsername
) )
......
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