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

Merge branch 'feature/messenger-example' into 'development'

Update examples

See merge request elixxir/elixxir-dapps-sdk-swift!37
parents eb99dd5b 44ea057d
Branches
Tags
2 merge requests!102Release 1.0.0,!37Update examples
Showing
with 28 additions and 1194 deletions
......@@ -34,6 +34,34 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XXClientTests"
BuildableName = "XXClientTests"
BlueprintName = "XXClientTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "XXMessengerClientTests"
BuildableName = "XXMessengerClientTests"
BlueprintName = "XXMessengerClientTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
</dict>
</plist>
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "example-app-icon-export"
BuildableName = "example-app-icon-export"
BlueprintName = "example-app-icon-export"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "example-app-icon-export"
BuildableName = "example-app-icon-export"
BlueprintName = "example-app-icon-export"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "example-app-icon-export"
BuildableName = "example-app-icon-export"
BlueprintName = "example-app-icon-export"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
// swift-tools-version: 5.6
import PackageDescription
let package = Package(
name: "example-app-icon",
platforms: [
.macOS(.v12),
],
products: [
.library(
name: "ExampleAppIcon",
targets: ["ExampleAppIcon"]
),
.executable(
name: "example-app-icon-export",
targets: ["ExampleAppIconExport"]
),
],
dependencies: [
.package(
url: "https://github.com/darrarski/swiftui-app-icon-creator.git",
.upToNextMajor(from: "1.2.0")
),
],
targets: [
.target(
name: "ExampleAppIcon",
dependencies: [
.product(
name: "AppIconCreator",
package: "swiftui-app-icon-creator"
),
]
),
.executableTarget(
name: "ExampleAppIconExport",
dependencies: [
.target(name: "ExampleAppIcon"),
]
)
]
)
import SwiftUI
import AppIconCreator
public struct ExampleAppIconView: View {
public init() {}
public var body: some View {
GeometryReader { geometry in
ZStack {
Image(systemName: "cube.transparent")
.resizable()
.scaledToFit()
.foregroundColor(.black.opacity(0.2))
.padding(geometry.size.width * 0.1)
.mask(
ZStack {
Rectangle()
Image(systemName: "cube")
.resizable()
.scaledToFit()
.blendMode(.destinationOut)
.padding(geometry.size.width * 0.1)
Circle()
.blendMode(.destinationOut)
.padding(geometry.size.width * 0.24)
}
)
Circle()
.fill(.black.opacity(0.3))
.padding(geometry.size.width * 0.3)
.mask {
ZStack {
Rectangle()
Image(systemName: "cube")
.resizable()
.scaledToFit()
.blendMode(.destinationOut)
.padding(geometry.size.width * 0.1)
}
}
Image(systemName: "cube")
.resizable()
.scaledToFit()
.foregroundColor(.black.opacity(0.5))
.padding(geometry.size.width * 0.1)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background {
LinearGradient(
gradient: Gradient(colors: [
Color(cgColor: CGColor(red: 0.49, green: 0.94, blue: 0.94, alpha: 1)),
Color(cgColor: CGColor(red: 0.16, green: 0.81, blue: 0.86, alpha: 1)),
]),
startPoint: .top,
endPoint: .bottom
)
}
}
}
}
struct ExampleAppIconView_Previews: PreviewProvider {
static var previews: some View {
IconPreviews(
icon: ExampleAppIconView(),
configs: .iOS
)
}
}
import AppIconCreator
import ExampleAppIcon
import Foundation
extension URL {
func deletingLastPathComponent() -> URL {
var url = self
url.deleteLastPathComponent()
return url
}
}
let exportURL = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("ExampleApp (iOS)")
.appendingPathComponent("Assets.xcassets")
.appendingPathComponent("AppIcon.appiconset")
[IconImage]
.images(for: ExampleAppIconView(), with: .iOS)
.forEach { $0.save(to: exportURL) }
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
<?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>
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 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 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment