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

Remove outdated example project

parent eb99dd5b
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!37Update examples
Showing
with 0 additions and 1659 deletions
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SessionFeature"
BuildableName = "SessionFeature"
BlueprintName = "SessionFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SessionFeatureTests"
BuildableName = "SessionFeatureTests"
BlueprintName = "SessionFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SessionFeature"
BuildableName = "SessionFeature"
BlueprintName = "SessionFeature"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppFeature"
BuildableName = "AppFeature"
BlueprintName = "AppFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ErrorFeature"
BuildableName = "ErrorFeature"
BlueprintName = "ErrorFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "LandingFeature"
BuildableName = "LandingFeature"
BlueprintName = "LandingFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyContactFeature"
BuildableName = "MyContactFeature"
BlueprintName = "MyContactFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyIdentityFeature"
BuildableName = "MyIdentityFeature"
BlueprintName = "MyIdentityFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SessionFeature"
BuildableName = "SessionFeature"
BlueprintName = "SessionFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppFeatureTests"
BuildableName = "AppFeatureTests"
BlueprintName = "AppFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ErrorFeatureTests"
BuildableName = "ErrorFeatureTests"
BlueprintName = "ErrorFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "LandingFeatureTests"
BuildableName = "LandingFeatureTests"
BlueprintName = "LandingFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyContactFeatureTests"
BuildableName = "MyContactFeatureTests"
BlueprintName = "MyContactFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyIdentityFeatureTests"
BuildableName = "MyIdentityFeatureTests"
BlueprintName = "MyIdentityFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SessionFeatureTests"
BuildableName = "SessionFeatureTests"
BlueprintName = "SessionFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppFeature"
BuildableName = "AppFeature"
BlueprintName = "AppFeature"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
// swift-tools-version: 5.6
import PackageDescription
let swiftSettings: [SwiftSetting] = [
.unsafeFlags(
[
"-Xfrontend", "-debug-time-function-bodies",
"-Xfrontend", "-debug-time-expression-type-checking",
],
.when(configuration: .debug)
),
]
let package = Package(
name: "example-app",
platforms: [
.iOS(.v15),
],
products: [
.library(name: "AppFeature", targets: ["AppFeature"]),
.library(name: "ErrorFeature", targets: ["ErrorFeature"]),
.library(name: "LandingFeature", targets: ["LandingFeature"]),
.library(name: "SessionFeature", targets: ["SessionFeature"]),
],
dependencies: [
.package(
path: "../../"
),
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture.git",
.upToNextMajor(from: "0.39.0")
),
.package(
url: "https://github.com/darrarski/swift-composable-presentation.git",
.upToNextMajor(from: "0.5.2")
),
.package(
url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
.upToNextMajor(from: "4.2.2")
),
.package(
url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
.upToNextMajor(from: "0.4.0")
),
],
targets: [
.target(
name: "AppFeature",
dependencies: [
.target(name: "ErrorFeature"),
.target(name: "LandingFeature"),
.target(name: "SessionFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "KeychainAccess", package: "KeychainAccess"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "AppFeatureTests",
dependencies: [
.target(name: "AppFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "ErrorFeature",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "ErrorFeatureTests",
dependencies: [
.target(name: "ErrorFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "LandingFeature",
dependencies: [
.target(name: "ErrorFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "LandingFeatureTests",
dependencies: [
.target(name: "LandingFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "SessionFeature",
dependencies: [
.target(name: "ErrorFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "SessionFeatureTests",
dependencies: [
.target(name: "SessionFeature"),
],
swiftSettings: swiftSettings
),
]
)
import Combine
import ComposableArchitecture
import ErrorFeature
import LandingFeature
import SessionFeature
import SwiftUI
import XXClient
@main
struct App: SwiftUI.App {
var body: some Scene {
WindowGroup {
AppView(store: Store(
initialState: AppState(),
reducer: appReducer,
environment: .live()
))
}
}
}
extension AppEnvironment {
static func live() -> AppEnvironment {
let cMixSubject = CurrentValueSubject<CMix?, Never>(nil)
let mainScheduler = DispatchQueue.main.eraseToAnyScheduler()
let bgScheduler = DispatchQueue(
label: "xx.network.dApps.ExampleApp.bg",
qos: .background
).eraseToAnyScheduler()
return AppEnvironment(
makeId: UUID.init,
hasCMix: { cMixSubject.map { $0 != nil }.eraseToAnyPublisher() },
mainScheduler: mainScheduler,
landing: LandingEnvironment(
cMixManager: .live(
passwordStorage: .keychain
),
setCMix: { cMixSubject.value = $0 },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler,
error: ErrorEnvironment()
),
session: SessionEnvironment(
getCMix: { cMixSubject.value },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler,
error: ErrorEnvironment()
)
)
}
}
import Combine
import ComposableArchitecture
import ComposablePresentation
import LandingFeature
import SessionFeature
import XCTestDynamicOverlay
struct AppState: Equatable {
enum Scene: Equatable {
case landing(LandingState)
case session(SessionState)
}
var id: UUID = UUID()
var scene: Scene = .landing(LandingState(id: UUID()))
}
extension AppState.Scene {
var asLanding: LandingState? {
get {
guard case .landing(let state) = self else { return nil }
return state
}
set {
guard let newValue = newValue else { return }
self = .landing(newValue)
}
}
var asSession: SessionState? {
get {
guard case .session(let state) = self else { return nil }
return state
}
set {
guard let newValue = newValue else { return }
self = .session(newValue)
}
}
}
enum AppAction: Equatable {
case viewDidLoad
case cMixDidChange(hasCMix: Bool)
case landing(LandingAction)
case session(SessionAction)
}
struct AppEnvironment {
var makeId: () -> UUID
var hasCMix: () -> AnyPublisher<Bool, Never>
var mainScheduler: AnySchedulerOf<DispatchQueue>
var landing: LandingEnvironment
var session: SessionEnvironment
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment>
{ state, action, env in
enum HasCMixEffectId {}
switch action {
case .viewDidLoad:
return env.hasCMix()
.removeDuplicates()
.map(AppAction.cMixDidChange(hasCMix:))
.receive(on: env.mainScheduler)
.eraseToEffect()
.cancellable(id: HasCMixEffectId.self, cancelInFlight: true)
case .cMixDidChange(let hasClient):
if hasClient {
let sessionState = state.scene.asSession ?? SessionState(id: env.makeId())
state.scene = .session(sessionState)
} else {
let landingState = state.scene.asLanding ?? LandingState(id: env.makeId())
state.scene = .landing(landingState)
}
return .none
case .landing(_), .session(_):
return .none
}
}
.presenting(
landingReducer,
state: .keyPath(\.scene.asLanding),
id: .notNil(),
action: /AppAction.landing,
environment: \.landing
)
.presenting(
sessionReducer,
state: .keyPath(\.scene.asSession),
id: .notNil(),
action: /AppAction.session,
environment: \.session
)
extension AppEnvironment {
static let unimplemented = AppEnvironment(
makeId: XCTUnimplemented("\(Self.self).makeId", placeholder: UUID()),
hasCMix: XCTUnimplemented("\(Self.self).hasCMix", placeholder: Empty().eraseToAnyPublisher()),
mainScheduler: .unimplemented,
landing: .unimplemented,
session: .unimplemented
)
}
import ComposableArchitecture
import LandingFeature
import SessionFeature
import SwiftUI
struct AppView: View {
let store: Store<AppState, AppAction>
struct ViewState: Equatable {
enum Scene: Equatable {
case landing
case session
}
let scene: Scene
init(state: AppState) {
switch state.scene {
case .landing(_):
self.scene = .landing
case .session(_):
self.scene = .session
}
}
}
var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
ZStack {
SwitchStore(store.scope(state: \.scene)) {
CaseLet(
state: /AppState.Scene.landing,
action: AppAction.landing,
then: { store in
NavigationView {
LandingView(store: store)
}
.navigationViewStyle(.stack)
.transition(.asymmetric(
insertion: .move(edge: .leading),
removal: .opacity
))
}
)
CaseLet(
state: /AppState.Scene.session,
action: AppAction.session,
then: { store in
NavigationView {
SessionView(store: store)
}
.navigationViewStyle(.stack)
.transition(.asymmetric(
insertion: .move(edge: .trailing),
removal: .opacity
))
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.animation(.default, value: viewStore.scene)
.task {
viewStore.send(.viewDidLoad)
}
}
}
}
#if DEBUG
struct AppView_Previews: PreviewProvider {
static var previews: some View {
AppView(store: Store(
initialState: AppState(),
reducer: .empty,
environment: ()
))
}
}
#endif
import KeychainAccess
import XXClient
extension PasswordStorage {
static let keychain: PasswordStorage = {
let keychain = KeychainAccess.Keychain(
service: "xx.network.dApps.ExampleApp"
)
return PasswordStorage(
save: { password in keychain[data: "password"] = password},
load: { try keychain[data: "password"] ?? { throw MissingPasswordError() }() }
)
}()
}
import ComposableArchitecture
import XCTestDynamicOverlay
public struct ErrorState: Equatable {
public init(error: NSError) {
self.error = error
}
public var error: NSError
}
public enum ErrorAction: Equatable {}
public struct ErrorEnvironment {
public init() {}
}
public let errorReducer = Reducer<ErrorState, ErrorAction, ErrorEnvironment>.empty
extension ErrorEnvironment {
public static let unimplemented = ErrorEnvironment()
}
import ComposableArchitecture
import SwiftUI
public struct ErrorView: View {
public init(store: Store<ErrorState, ErrorAction>) {
self.store = store
}
let store: Store<ErrorState, ErrorAction>
@Environment(\.dismiss) var dismiss
struct ViewState: Equatable {
let error: NSError
init(state: ErrorState) {
error = state.error
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
NavigationView {
Form {
Text("\(viewStore.error)")
}
.navigationTitle("Error")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
}
}
}
}
}
}
}
#if DEBUG
public struct ErrorView_Previews: PreviewProvider {
public static var previews: some View {
ErrorView(store: .init(
initialState: .init(
error: NSError(domain: "preview", code: 1234)
),
reducer: .empty,
environment: ()
))
}
}
#endif
import Combine
import ComposableArchitecture
import ErrorFeature
import XCTestDynamicOverlay
import XXClient
public struct LandingState: Equatable {
public init(
id: UUID,
hasStoredCMix: Bool = false,
isMakingCMix: Bool = false,
isRemovingCMix: Bool = false,
error: ErrorState? = nil
) {
self.id = id
self.hasStoredCMix = hasStoredCMix
self.isMakingCMix = isMakingCMix
self.isRemovingCMix = isRemovingCMix
self.error = error
}
var id: UUID
var hasStoredCMix: Bool
var isMakingCMix: Bool
var isRemovingCMix: Bool
var error: ErrorState?
}
public enum LandingAction: Equatable {
case viewDidLoad
case makeCMix
case didMakeCMix
case didFailMakingCMix(NSError)
case removeStoredCMix
case didRemoveStoredCMix
case didFailRemovingStoredCMix(NSError)
case didDismissError
case error(ErrorAction)
}
public struct LandingEnvironment {
public init(
cMixManager: CMixManager,
setCMix: @escaping (CMix) -> Void,
bgScheduler: AnySchedulerOf<DispatchQueue>,
mainScheduler: AnySchedulerOf<DispatchQueue>,
error: ErrorEnvironment
) {
self.cMixManager = cMixManager
self.setCMix = setCMix
self.bgScheduler = bgScheduler
self.mainScheduler = mainScheduler
self.error = error
}
public var cMixManager: CMixManager
public var setCMix: (CMix) -> Void
public var bgScheduler: AnySchedulerOf<DispatchQueue>
public var mainScheduler: AnySchedulerOf<DispatchQueue>
public var error: ErrorEnvironment
}
public let landingReducer = Reducer<LandingState, LandingAction, LandingEnvironment>
{ state, action, env in
switch action {
case .viewDidLoad:
state.hasStoredCMix = env.cMixManager.hasStorage()
return .none
case .makeCMix:
state.isMakingCMix = true
return Effect.future { fulfill in
do {
if env.cMixManager.hasStorage() {
env.setCMix(try env.cMixManager.load())
} else {
env.setCMix(try env.cMixManager.create())
}
fulfill(.success(.didMakeCMix))
} catch {
fulfill(.success(.didFailMakingCMix(error as NSError)))
}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didMakeCMix:
state.isMakingCMix = false
state.hasStoredCMix = env.cMixManager.hasStorage()
return .none
case .didFailMakingCMix(let error):
state.isMakingCMix = false
state.hasStoredCMix = env.cMixManager.hasStorage()
state.error = ErrorState(error: error)
return .none
case .removeStoredCMix:
state.isRemovingCMix = true
return Effect.future { fulfill in
do {
try env.cMixManager.remove()
fulfill(.success(.didRemoveStoredCMix))
} catch {
fulfill(.success(.didFailRemovingStoredCMix(error as NSError)))
}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didRemoveStoredCMix:
state.isRemovingCMix = false
state.hasStoredCMix = env.cMixManager.hasStorage()
return .none
case .didFailRemovingStoredCMix(let error):
state.isRemovingCMix = false
state.hasStoredCMix = env.cMixManager.hasStorage()
state.error = ErrorState(error: error)
return .none
case .didDismissError:
state.error = nil
return .none
}
}
.presenting(
errorReducer,
state: .keyPath(\.error),
id: .keyPath(\.?.error),
action: /LandingAction.error,
environment: \.error
)
extension LandingEnvironment {
public static let unimplemented = LandingEnvironment(
cMixManager: .unimplemented,
setCMix: XCTUnimplemented("\(Self.self).setCMix"),
bgScheduler: .unimplemented,
mainScheduler: .unimplemented,
error: .unimplemented
)
}
import ComposableArchitecture
import ComposablePresentation
import ErrorFeature
import SwiftUI
public struct LandingView: View {
public init(store: Store<LandingState, LandingAction>) {
self.store = store
}
let store: Store<LandingState, LandingAction>
struct ViewState: Equatable {
let hasStoredCMix: Bool
let isMakingCMix: Bool
let isRemovingCMix: Bool
init(state: LandingState) {
hasStoredCMix = state.hasStoredCMix
isMakingCMix = state.isMakingCMix
isRemovingCMix = state.isRemovingCMix
}
var isLoading: Bool {
isMakingCMix ||
isRemovingCMix
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Form {
Button {
viewStore.send(.makeCMix)
} label: {
HStack {
Text(viewStore.hasStoredCMix ? "Load stored cMix" : "Create new cMix")
Spacer()
if viewStore.isMakingCMix {
ProgressView()
}
}
}
if viewStore.hasStoredCMix {
Button(role: .destructive) {
viewStore.send(.removeStoredCMix)
} label: {
HStack {
Text("Remove stored cMix")
Spacer()
if viewStore.isRemovingCMix {
ProgressView()
}
}
}
}
}
.navigationTitle("Landing")
.disabled(viewStore.isLoading)
.task {
viewStore.send(.viewDidLoad)
}
.sheet(
store.scope(
state: \.error,
action: LandingAction.error
),
onDismiss: {
viewStore.send(.didDismissError)
},
content: ErrorView.init(store:)
)
}
}
}
#if DEBUG
public struct LandingView_Previews: PreviewProvider {
public static var previews: some View {
NavigationView {
LandingView(store: .init(
initialState: .init(id: UUID()),
reducer: .empty,
environment: ()
))
}
.navigationViewStyle(.stack)
}
}
#endif
import SwiftUI
import XXClient
struct NetworkFollowerStatusView: View {
var status: NetworkFollowerStatus?
var body: some View {
switch status {
case .stopped:
Label("Stopped", systemImage: "stop.fill")
case .running:
Label("Running", systemImage: "play.fill")
case .stopping:
Label("Stopping...", systemImage: "stop")
case .unknown(let code):
Label("Status \(code)", systemImage: "questionmark")
case .none:
Label("Unknown", systemImage: "questionmark")
}
}
}
#if DEBUG
struct NetworkFollowerStatusView_Previews: PreviewProvider {
static var previews: some View {
Group {
NetworkFollowerStatusView(status: .stopped)
NetworkFollowerStatusView(status: .running)
NetworkFollowerStatusView(status: .stopping)
NetworkFollowerStatusView(status: .unknown(code: -1))
NetworkFollowerStatusView(status: nil)
}
.previewLayout(.sizeThatFits)
}
}
#endif
import SwiftUI
struct NetworkHealthStatusView: View {
var status: Bool?
var body: some View {
switch status {
case .some(true):
Label("Healthy", systemImage: "wifi")
.foregroundColor(.green)
case .some(false):
Label("Unhealthy", systemImage: "bolt.horizontal.fill")
.foregroundColor(.red)
case .none:
Label("Unknown", systemImage: "questionmark")
}
}
}
#if DEBUG
struct NetworkHealthStatusView_Previews: PreviewProvider {
static var previews: some View {
Group {
NetworkHealthStatusView(status: true)
NetworkHealthStatusView(status: false)
NetworkHealthStatusView(status: nil)
}
.previewLayout(.sizeThatFits)
}
}
#endif
import Combine
import ComposableArchitecture
import ErrorFeature
import XCTestDynamicOverlay
import XXClient
public struct SessionState: Equatable {
public init(
id: UUID,
networkFollowerStatus: NetworkFollowerStatus? = nil,
isNetworkHealthy: Bool? = nil,
error: ErrorState? = nil
) {
self.id = id
self.networkFollowerStatus = networkFollowerStatus
self.isNetworkHealthy = isNetworkHealthy
self.error = error
}
public var id: UUID
public var networkFollowerStatus: NetworkFollowerStatus?
public var isNetworkHealthy: Bool?
public var error: ErrorState?
}
public enum SessionAction: Equatable {
case viewDidLoad
case updateNetworkFollowerStatus
case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?)
case runNetworkFollower(Bool)
case networkFollowerDidFail(NSError)
case monitorNetworkHealth(Bool)
case didUpdateNetworkHealth(Bool?)
case didDismissError
case error(ErrorAction)
}
public struct SessionEnvironment {
public init(
getCMix: @escaping () -> CMix?,
bgScheduler: AnySchedulerOf<DispatchQueue>,
mainScheduler: AnySchedulerOf<DispatchQueue>,
error: ErrorEnvironment
) {
self.getCMix = getCMix
self.bgScheduler = bgScheduler
self.mainScheduler = mainScheduler
self.error = error
}
public var getCMix: () -> CMix?
public var bgScheduler: AnySchedulerOf<DispatchQueue>
public var mainScheduler: AnySchedulerOf<DispatchQueue>
public var error: ErrorEnvironment
}
public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
{ state, action, env in
switch action {
case .viewDidLoad:
return .merge([
.init(value: .updateNetworkFollowerStatus),
.init(value: .monitorNetworkHealth(true)),
])
case .updateNetworkFollowerStatus:
return Effect.future { fulfill in
let status = env.getCMix()?.networkFollowerStatus()
fulfill(.success(.didUpdateNetworkFollowerStatus(status)))
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didUpdateNetworkFollowerStatus(let status):
state.networkFollowerStatus = status
return .none
case .runNetworkFollower(let start):
return Effect.run { subscriber in
do {
if start {
try env.getCMix()?.startNetworkFollower(timeoutMS: 30_000)
} else {
try env.getCMix()?.stopNetworkFollower()
}
} catch {
subscriber.send(.networkFollowerDidFail(error as NSError))
}
let status = env.getCMix()?.networkFollowerStatus()
subscriber.send(.didUpdateNetworkFollowerStatus(status))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .networkFollowerDidFail(let error):
state.error = ErrorState(error: error)
return .none
case .monitorNetworkHealth(let start):
struct MonitorEffectId: Hashable {
var id: UUID
}
let effectId = MonitorEffectId(id: state.id)
if start {
return Effect.run { subscriber in
let callback = HealthCallback { isHealthy in
subscriber.send(.didUpdateNetworkHealth(isHealthy))
}
let cancellable = env.getCMix()?.addHealthCallback(callback)
return AnyCancellable {
cancellable?.cancel()
}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
.cancellable(id: effectId, cancelInFlight: true)
} else {
return Effect.cancel(id: effectId)
.subscribe(on: env.bgScheduler)
.eraseToEffect()
}
case .didUpdateNetworkHealth(let isHealthy):
state.isNetworkHealthy = isHealthy
return .none
case .didDismissError:
state.error = nil
return .none
case .error(_):
return .none
}
}
.presenting(
errorReducer,
state: .keyPath(\.error),
id: .keyPath(\.?.error),
action: /SessionAction.error,
environment: \.error
)
extension SessionEnvironment {
public static let unimplemented = SessionEnvironment(
getCMix: XCTUnimplemented("\(Self.self).getCMix"),
bgScheduler: .unimplemented,
mainScheduler: .unimplemented,
error: .unimplemented
)
}
import ComposableArchitecture
import ComposablePresentation
import ErrorFeature
import SwiftUI
import XXClient
public struct SessionView: View {
public init(store: Store<SessionState, SessionAction>) {
self.store = store
}
let store: Store<SessionState, SessionAction>
struct ViewState: Equatable {
let networkFollowerStatus: NetworkFollowerStatus?
let isNetworkHealthy: Bool?
init(state: SessionState) {
networkFollowerStatus = state.networkFollowerStatus
isNetworkHealthy = state.isNetworkHealthy
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Form {
Section {
NetworkFollowerStatusView(status: viewStore.networkFollowerStatus)
Button {
viewStore.send(.runNetworkFollower(true))
} label: {
Text("Start")
}
.disabled(viewStore.networkFollowerStatus != .stopped)
Button {
viewStore.send(.runNetworkFollower(false))
} label: {
Text("Stop")
}
.disabled(viewStore.networkFollowerStatus != .running)
} header: {
Text("Network follower")
}
Section {
NetworkHealthStatusView(status: viewStore.isNetworkHealthy)
} header: {
Text("Network health")
}
}
.navigationTitle("Session")
.task {
viewStore.send(.viewDidLoad)
}
.sheet(
store.scope(
state: \.error,
action: SessionAction.error
),
onDismiss: {
viewStore.send(.didDismissError)
},
content: ErrorView.init(store:)
)
}
}
}
#if DEBUG
public struct SessionView_Previews: PreviewProvider {
public static var previews: some View {
SessionView(store: .init(
initialState: .init(id: UUID()),
reducer: .empty,
environment: ()
))
}
}
#endif
import Combine
import ComposableArchitecture
import LandingFeature
import SessionFeature
import XCTest
@testable import AppFeature
final class AppFeatureTests: XCTestCase {
func testViewDidLoad() throws {
let newId = UUID()
let hasCMix = PassthroughSubject<Bool, Never>()
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: .unimplemented
)
store.environment.makeId = { newId }
store.environment.hasCMix = { hasCMix.eraseToAnyPublisher() }
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.viewDidLoad)
hasCMix.send(false)
mainScheduler.advance()
store.receive(.cMixDidChange(hasCMix: false))
hasCMix.send(true)
mainScheduler.advance()
store.receive(.cMixDidChange(hasCMix: true)) {
$0.scene = .session(SessionState(id: newId))
}
hasCMix.send(true)
mainScheduler.advance()
hasCMix.send(false)
mainScheduler.advance()
store.receive(.cMixDidChange(hasCMix: false)) {
$0.scene = .landing(LandingState(id: newId))
}
hasCMix.send(completion: .finished)
mainScheduler.advance()
}
}
import XCTest
@testable import ErrorFeature
final class ErrorFeatureTests: XCTestCase {
func testExample() {
XCTAssert(true)
}
}
import ComposableArchitecture
import ErrorFeature
import XCTest
@testable import LandingFeature
final class LandingFeatureTests: XCTestCase {
func testViewDidLoad() throws {
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { true }
store.send(.viewDidLoad) {
$0.hasStoredCMix = true
}
}
func testCreateCMix() {
var hasStoredCMix = false
var didSetCMix = false
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { hasStoredCMix }
store.environment.cMixManager.create.run = { .unimplemented }
store.environment.setCMix = { _ in didSetCMix = true }
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.makeCMix) {
$0.isMakingCMix = true
}
bgScheduler.advance()
XCTAssertTrue(didSetCMix)
hasStoredCMix = true
mainScheduler.advance()
store.receive(.didMakeCMix) {
$0.isMakingCMix = false
$0.hasStoredCMix = true
}
}
func testLoadStoredCMix() {
var didSetCMix = false
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { true }
store.environment.cMixManager.load.run = { .unimplemented }
store.environment.setCMix = { _ in didSetCMix = true }
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.makeCMix) {
$0.isMakingCMix = true
}
bgScheduler.advance()
XCTAssertTrue(didSetCMix)
mainScheduler.advance()
store.receive(.didMakeCMix) {
$0.isMakingCMix = false
$0.hasStoredCMix = true
}
}
func testMakeCMixFailure() {
let error = NSError(domain: "test", code: 1234)
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { false }
store.environment.cMixManager.create.run = { throw error }
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.makeCMix) {
$0.isMakingCMix = true
}
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didFailMakingCMix(error)) {
$0.isMakingCMix = false
$0.hasStoredCMix = false
$0.error = ErrorState(error: error)
}
}
func testRemoveStoredCMix() {
var hasStoredCMix = true
var didRemoveCMix = false
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { hasStoredCMix }
store.environment.cMixManager.remove.run = { didRemoveCMix = true }
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.removeStoredCMix) {
$0.isRemovingCMix = true
}
bgScheduler.advance()
XCTAssertTrue(didRemoveCMix)
hasStoredCMix = false
mainScheduler.advance()
store.receive(.didRemoveStoredCMix) {
$0.isRemovingCMix = false
$0.hasStoredCMix = false
}
}
func testRemoveStoredCMixFailure() {
let error = NSError(domain: "test", code: 1234)
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: LandingState(id: UUID()),
reducer: landingReducer,
environment: .unimplemented
)
store.environment.cMixManager.hasStorage.run = { true }
store.environment.cMixManager.remove.run = { throw error }
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.removeStoredCMix) {
$0.isRemovingCMix = true
}
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didFailRemovingStoredCMix(error)) {
$0.isRemovingCMix = false
$0.hasStoredCMix = true
$0.error = ErrorState(error: error)
}
}
}
import ComposableArchitecture
import ErrorFeature
import XCTest
import XXClient
@testable import SessionFeature
final class SessionFeatureTests: XCTestCase {
func testViewDidLoad() {
var networkFollowerStatus: NetworkFollowerStatus!
var didStartMonitoringNetworkHealth = 0
var didStopMonitoringNetworkHealth = 0
var networkHealthCallback: HealthCallback!
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: SessionState(id: UUID()),
reducer: sessionReducer,
environment: .unimplemented
)
store.environment.getCMix = {
var cMix = CMix.unimplemented
cMix.networkFollowerStatus.run = { networkFollowerStatus }
cMix.addHealthCallback.run = { callback in
networkHealthCallback = callback
didStartMonitoringNetworkHealth += 1
return Cancellable {
didStopMonitoringNetworkHealth += 1
}
}
return cMix
}
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.viewDidLoad)
store.receive(.updateNetworkFollowerStatus)
store.receive(.monitorNetworkHealth(true))
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
$0.networkFollowerStatus = .stopped
}
XCTAssertEqual(didStartMonitoringNetworkHealth, 1)
XCTAssertEqual(didStopMonitoringNetworkHealth, 0)
networkHealthCallback.handle(true)
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didUpdateNetworkHealth(true)) {
$0.isNetworkHealthy = true
}
store.send(.monitorNetworkHealth(false))
bgScheduler.advance()
XCTAssertEqual(didStartMonitoringNetworkHealth, 1)
XCTAssertEqual(didStopMonitoringNetworkHealth, 1)
}
func testStartStopNetworkFollower() {
var networkFollowerStatus: NetworkFollowerStatus!
var didStartNetworkFollowerWithTimeout = [Int]()
var didStopNetworkFollower = 0
var networkFollowerStartError: NSError?
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
let store = TestStore(
initialState: SessionState(id: UUID()),
reducer: sessionReducer,
environment: .unimplemented
)
store.environment.getCMix = {
var cMix = CMix.unimplemented
cMix.networkFollowerStatus.run = { networkFollowerStatus }
cMix.startNetworkFollower.run = {
didStartNetworkFollowerWithTimeout.append($0)
if let error = networkFollowerStartError {
throw error
}
}
cMix.stopNetworkFollower.run = {
didStopNetworkFollower += 1
}
return cMix
}
store.environment.bgScheduler = bgScheduler.eraseToAnyScheduler()
store.environment.mainScheduler = mainScheduler.eraseToAnyScheduler()
store.send(.runNetworkFollower(true))
networkFollowerStatus = .running
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
XCTAssertEqual(didStopNetworkFollower, 0)
store.receive(.didUpdateNetworkFollowerStatus(.running)) {
$0.networkFollowerStatus = .running
}
store.send(.runNetworkFollower(false))
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
XCTAssertEqual(didStopNetworkFollower, 1)
store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
$0.networkFollowerStatus = .stopped
}
store.send(.runNetworkFollower(true))
networkFollowerStartError = NSError(domain: "test", code: 1234)
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000, 30_000])
XCTAssertEqual(didStopNetworkFollower, 1)
store.receive(.networkFollowerDidFail(networkFollowerStartError!)) {
$0.error = ErrorState(error: networkFollowerStartError!)
}
store.receive(.didUpdateNetworkFollowerStatus(.stopped))
store.send(.didDismissError) {
$0.error = nil
}
}
}
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