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
import SwiftUI
public struct RegisterView: View {
public init(store: Store<RegisterState, RegisterAction>) {
public init(store: StoreOf<RegisterComponent>) {
self.store = store
}
let store: Store<RegisterState, RegisterAction>
@FocusState var focusedField: RegisterState.Field?
let store: StoreOf<RegisterComponent>
@FocusState var focusedField: RegisterComponent.State.Field?
struct ViewState: Equatable {
init(_ state: RegisterState) {
init(_ state: RegisterComponent.State) {
focusedField = state.focusedField
username = state.username
isRegistering = state.isRegistering
failure = state.failure
}
var focusedField: RegisterState.Field?
var focusedField: RegisterComponent.State.Field?
var username: String
var isRegistering: Bool
var failure: String?
......@@ -31,7 +31,7 @@ public struct RegisterView: View {
TextField(
text: viewStore.binding(
get: \.username,
send: { RegisterAction.set(\.$username, $0) }
send: { RegisterComponent.Action.set(\.$username, $0) }
),
prompt: Text("Enter username"),
label: { Text("Username") }
......@@ -78,9 +78,8 @@ public struct RegisterView: View {
public struct RegisterView_Previews: PreviewProvider {
public static var previews: some View {
RegisterView(store: Store(
initialState: RegisterState(),
reducer: .empty,
environment: ()
initialState: RegisterComponent.State(),
reducer: EmptyReducer()
))
}
}
......
......@@ -6,7 +6,7 @@ import XXMessengerClient
import XXModels
@testable import RegisterFeature
final class RegisterFeatureTests: XCTestCase {
final class RegisterComponentTests: XCTestCase {
func testRegister() throws {
let now = Date()
let username = "registering-username"
......@@ -26,24 +26,23 @@ final class RegisterFeatureTests: XCTestCase {
var dbDidSaveContact: [XXModels.Contact] = []
let store = TestStore(
initialState: RegisterState(),
reducer: registerReducer,
environment: .unimplemented
initialState: RegisterComponent.State(),
reducer: RegisterComponent()
)
store.environment.now = { now }
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.messenger.register.run = { username in
store.dependencies.app.now = { now }
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
store.dependencies.app.messenger.register.run = { username in
messengerDidRegisterUsername.append(username)
}
store.environment.messenger.myContact.run = { includeFacts in
store.dependencies.app.messenger.myContact.run = { includeFacts in
didGetMyContact.append(includeFacts)
var contact = XXClient.Contact.unimplemented(myContactData)
contact.getIdFromContact.run = { _ in myContactId }
contact.getFactsFromContact.run = { _ in myContactFacts }
return contact
}
store.environment.db.run = {
store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented
db.saveContact.run = { contact in
dbDidSaveContact.append(contact)
......@@ -87,17 +86,16 @@ final class RegisterFeatureTests: XCTestCase {
let error = Error()
let store = TestStore(
initialState: RegisterState(),
reducer: registerReducer,
environment: .unimplemented
initialState: RegisterComponent.State(),
reducer: RegisterComponent()
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.db.run = { throw error }
store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler()
store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler()
store.dependencies.app.dbManager.getDB.run = { throw error }
store.send(.registerTapped) {
$0.isRegistering = true
......@@ -117,18 +115,17 @@ final class RegisterFeatureTests: XCTestCase {
let error = Error()
let store = TestStore(
initialState: RegisterState(),
reducer: registerReducer,
environment: .unimplemented
initialState: RegisterComponent.State(),
reducer: RegisterComponent()
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.db.run = { .unimplemented }
store.environment.messenger.register.run = { _ in throw error }
store.dependencies.app.mainQueue = mainQueue.eraseToAnyScheduler()
store.dependencies.app.bgQueue = bgQueue.eraseToAnyScheduler()
store.dependencies.app.dbManager.getDB.run = { .unimplemented }
store.dependencies.app.messenger.register.run = { _ in throw error }
store.send(.registerTapped) {
$0.isRegistering = true
......@@ -162,26 +159,25 @@ final class RegisterFeatureTests: XCTestCase {
var dbDidSaveContact: [XXModels.Contact] = []
let store = TestStore(
initialState: RegisterState(
initialState: RegisterComponent.State(
username: username
),
reducer: registerReducer,
environment: .unimplemented
reducer: RegisterComponent()
)
store.environment.now = { now }
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.messenger.register.run = { username in
store.dependencies.app.now = { now }
store.dependencies.app.mainQueue = .immediate
store.dependencies.app.bgQueue = .immediate
store.dependencies.app.messenger.register.run = { username in
messengerDidRegisterUsername.append(username)
}
store.environment.messenger.myContact.run = { includeFacts in
store.dependencies.app.messenger.myContact.run = { includeFacts in
didGetMyContact.append(includeFacts)
var contact = XXClient.Contact.unimplemented(myContactData)
contact.getIdFromContact.run = { _ in myContactId }
contact.getFactsFromContact.run = { _ in myContactFacts }
return contact
}
store.environment.db.run = {
store.dependencies.app.dbManager.getDB.run = {
var db: Database = .unimplemented
db.saveContact.run = { contact in
dbDidSaveContact.append(contact)
......@@ -207,7 +203,7 @@ final class RegisterFeatureTests: XCTestCase {
)
])
let failure = RegisterState.Error.usernameMismatch(
let failure = RegisterComponent.State.Error.usernameMismatch(
registering: username,
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