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

Fetch my contact

parent 6d0f1a18
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!98Messenger example - register, confirm, and unregister user facts
...@@ -125,7 +125,12 @@ extension AppEnvironment { ...@@ -125,7 +125,12 @@ extension AppEnvironment {
bgQueue: bgQueue, bgQueue: bgQueue,
contact: { contactEnvironment }, contact: { contactEnvironment },
myContact: { myContact: {
MyContactEnvironment() MyContactEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
} }
) )
}, },
......
import AppCore
import ComposableArchitecture import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct MyContactState: Equatable { public struct MyContactState: Equatable {
public init() {} public enum Field: String, Hashable {
case email
case phone
}
public init(
contact: XXModels.Contact? = nil,
focusedField: Field? = nil,
email: String = "",
phone: String = ""
) {
self.contact = contact
self.focusedField = focusedField
self.email = email
self.phone = phone
}
public var contact: XXModels.Contact?
@BindableState public var focusedField: Field?
@BindableState public var email: String
@BindableState public var phone: String
} }
public enum MyContactAction: Equatable { public enum MyContactAction: Equatable, BindableAction {
case start case start
case contactFetched(XXModels.Contact?)
case registerEmailTapped
case unregisterEmailTapped
case registerPhoneTapped
case unregisterPhoneTapped
case loadFactsTapped
case binding(BindingAction<MyContactState>)
} }
public struct MyContactEnvironment { public struct MyContactEnvironment {
public init() {} public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
} }
#if DEBUG #if DEBUG
extension MyContactEnvironment { extension MyContactEnvironment {
public static let unimplemented = MyContactEnvironment() public static let unimplemented = MyContactEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
} }
#endif #endif
public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment> public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
{ state, action, env in { state, action, env in
enum DBFetchEffectID {}
switch action { switch action {
case .start: case .start:
return Effect
.catching { try env.messenger.e2e.tryGet().getContact().getId() }
.tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) }
.flatMap { $0 }
.assertNoFailure()
.map(\.first)
.map(MyContactAction.contactFetched)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
case .contactFetched(let contact):
state.contact = contact
return .none
case .registerEmailTapped:
return .none
case .unregisterEmailTapped:
return .none
case .registerPhoneTapped:
return .none
case .unregisterPhoneTapped:
return .none
case .loadFactsTapped:
return .none
case .binding(_):
return .none return .none
} }
} }
.binding()
import ComposableArchitecture import ComposableArchitecture
import SwiftUI import SwiftUI
import XXModels
public struct MyContactView: View { public struct MyContactView: View {
public init(store: Store<MyContactState, MyContactAction>) { public init(store: Store<MyContactState, MyContactAction>) {
...@@ -7,17 +8,113 @@ public struct MyContactView: View { ...@@ -7,17 +8,113 @@ public struct MyContactView: View {
} }
let store: Store<MyContactState, MyContactAction> let store: Store<MyContactState, MyContactAction>
@FocusState var focusedField: MyContactState.Field?
struct ViewState: Equatable { struct ViewState: Equatable {
init(state: MyContactState) {} init(state: MyContactState) {
contact = state.contact
focusedField = state.focusedField
email = state.email
phone = state.phone
}
var contact: XXModels.Contact?
var focusedField: MyContactState.Field?
var email: String
var phone: String
} }
public var body: some View { public var body: some View {
WithViewStore(store, observe: ViewState.init) { viewStore in WithViewStore(store, observe: ViewState.init) { viewStore in
Form { Form {
Section {
Text(viewStore.contact?.username ?? "")
} header: {
Label("Username", systemImage: "person")
}
Section {
if let contact = viewStore.contact {
if let email = contact.email {
Text(email)
Button(role: .destructive) {
viewStore.send(.unregisterEmailTapped)
} label: {
Text("Unregister")
}
} else {
TextField(
text: viewStore.binding(
get: \.email,
send: { MyContactAction.set(\.$email, $0) }
),
prompt: Text("Enter email"),
label: { Text("Email") }
)
.focused($focusedField, equals: .email)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
Button {
viewStore.send(.registerEmailTapped)
} label: {
Text("Register")
}
}
} else {
Text("")
}
} header: {
Label("Email", systemImage: "envelope")
}
Section {
if let contact = viewStore.contact {
if let phone = contact.phone {
Text(phone)
Button(role: .destructive) {
viewStore.send(.unregisterPhoneTapped)
} label: {
Text("Unregister")
}
} else {
TextField(
text: viewStore.binding(
get: \.phone,
send: { MyContactAction.set(\.$phone, $0) }
),
prompt: Text("Enter phone"),
label: { Text("Phone") }
)
.focused($focusedField, equals: .phone)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
Button {
viewStore.send(.registerPhoneTapped)
} label: {
Text("Register")
}
}
} else {
Text("")
}
} header: {
Label("Phone", systemImage: "phone")
}
Section {
Button {
viewStore.send(.loadFactsTapped)
} label: {
Text("Load facts from client")
}
} header: {
Text("Actions")
}
} }
.navigationTitle("My Contact") .navigationTitle("My Contact")
.task { viewStore.send(.start) }
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
} }
} }
} }
...@@ -25,11 +122,13 @@ public struct MyContactView: View { ...@@ -25,11 +122,13 @@ public struct MyContactView: View {
#if DEBUG #if DEBUG
public struct MyContactView_Previews: PreviewProvider { public struct MyContactView_Previews: PreviewProvider {
public static var previews: some View { public static var previews: some View {
MyContactView(store: Store( NavigationView {
initialState: MyContactState(), MyContactView(store: Store(
reducer: .empty, initialState: MyContactState(),
environment: () reducer: .empty,
)) environment: ()
))
}
} }
} }
#endif #endif
import Combine
import ComposableArchitecture import ComposableArchitecture
import CustomDump
import XCTest import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import MyContactFeature @testable import MyContactFeature
final class MyContactFeatureTests: XCTestCase { final class MyContactFeatureTests: XCTestCase {
func testStart() { func testStart() {
let contactId = "contact-id".data(using: .utf8)!
let store = TestStore( let store = TestStore(
initialState: MyContactState(), initialState: MyContactState(),
reducer: myContactReducer, reducer: myContactReducer,
environment: .unimplemented environment: .unimplemented
) )
var dbDidFetchContacts: [XXModels.Contact.Query] = []
let dbContactsPublisher = 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 contactId }
return contact
}
return e2e
}
store.environment.db.run = {
var db: Database = .failing
db.fetchContactsPublisher.run = { query in
dbDidFetchContacts.append(query)
return dbContactsPublisher.eraseToAnyPublisher()
}
return db
}
store.send(.start) store.send(.start)
XCTAssertNoDifference(dbDidFetchContacts, [.init(id: [contactId])])
dbContactsPublisher.send([])
store.receive(.contactFetched(nil))
let contact = XXModels.Contact(id: contactId)
dbContactsPublisher.send([contact])
store.receive(.contactFetched(contact)) {
$0.contact = contact
}
dbContactsPublisher.send(completion: .finished)
}
func testRegisterEmail() {
let email = "test@email.com"
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.set(\.$email, email)) {
$0.email = email
}
store.send(.registerEmailTapped)
}
func testUnregisterEmail() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.unregisterEmailTapped)
}
func testRegisterPhone() {
let phone = "123456789"
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.set(\.$phone, phone)) {
$0.phone = phone
}
store.send(.registerPhoneTapped)
}
func testUnregisterPhone() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.unregisterPhoneTapped)
}
func testLoadFactsFromClient() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.loadFactsTapped)
} }
} }
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