Created
November 9, 2022 23:23
-
-
Save gohanlon/107850c1da7e963b40105a8b2c44ebfc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ComposableArchitecture | |
import Dependencies | |
import SwiftUI | |
// Based on Isowords's `ComposableGameCenter.LiveKey.LocalPlayerClient.live`: | |
// https://github.com/pointfreeco/isowords/blob/main/Sources/ComposableGameCenter/LiveKey.swift#L80 | |
struct AuthClient { | |
enum AuthStatus { | |
case loggedIn | |
case loggedOut | |
} | |
var status: @Sendable () async -> AuthStatus | |
var statusChanges: @Sendable () async -> AsyncStream<AuthStatus> | |
var authenticate: @Sendable () async throws -> Void | |
} | |
extension AuthClient: DependencyKey { | |
static var liveValue: Self = .live | |
} | |
extension DependencyValues { | |
var authClient: AuthClient { | |
get { self[AuthClient.self] } | |
set { self[AuthClient.self] = newValue } | |
} | |
} | |
extension AuthClient { | |
fileprivate class Listener { | |
let continuation: AsyncStream<AuthStatus>.Continuation | |
init(continuation: AsyncStream<AuthStatus>.Continuation) { | |
self.continuation = continuation | |
} | |
} | |
public static var live: Self = { | |
let authStatus = ActorIsolated(AuthStatus.loggedOut) | |
@Sendable | |
func networkRequest() async throws -> AuthStatus { | |
try await Task.sleep(for: .seconds(2)) // simulate network request | |
return .loggedIn | |
} | |
return Self( | |
status: { | |
return await authStatus.value | |
}, | |
statusChanges: { | |
print("ApiClient.Live.statusChanges") | |
let status = await authStatus.value | |
return AsyncStream { continuation in | |
let id = UUID() | |
let listener = Listener(continuation: continuation) | |
Self.listeners[id] = listener | |
continuation.onTermination = { _ in | |
Self.listeners[id] = nil | |
} | |
// Optional: emit the current value when a new subscriber starts listening | |
// continuation.yield(status) | |
} | |
}, | |
authenticate: { | |
print("ApiClient.Live.authenticate") | |
let newAuthStatus = try await networkRequest() | |
if await authStatus.value != newAuthStatus { | |
for listener in Self.listeners.values { | |
listener.continuation.yield(newAuthStatus) | |
} | |
} | |
await authStatus.setValue(newAuthStatus) | |
} | |
) | |
}() | |
private static var listeners: [UUID: Listener] = [:] | |
} | |
struct FeatureA: ReducerProtocol { | |
struct State: Equatable { | |
} | |
enum Action: Equatable { | |
case task | |
} | |
@Dependency(\.authClient) var authClient | |
var body: some ReducerProtocolOf<Self> { | |
Reduce { state, action in | |
switch action { | |
case .task: | |
return .run { _ in | |
for await status in await self.authClient.statusChanges() { | |
print("Feature A:", status) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct FeatureAView: View { | |
var store: StoreOf<FeatureA> | |
var body: some View { | |
Color.red | |
.overlay(Text("Feature A")) | |
.task { await ViewStore(self.store).send(.task).finish() } | |
} | |
} | |
struct FeatureB: ReducerProtocol { | |
struct State: Equatable { | |
} | |
enum Action: Equatable { | |
case task | |
} | |
@Dependency(\.authClient) var authClient | |
var body: some ReducerProtocolOf<Self> { | |
Reduce { state, action in | |
switch action { | |
case .task: | |
return .run { _ in | |
for await status in await self.authClient.statusChanges() { | |
print("Feature B:", status) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct FeatureBView: View { | |
var store: StoreOf<FeatureB> | |
var body: some View { | |
Color.green | |
.overlay(Text("Feature B")) | |
.task { await ViewStore(self.store).send(.task).finish() } | |
} | |
} | |
struct AppFeature: ReducerProtocol { | |
struct State: Equatable { | |
var featureA: FeatureA.State | |
var featureB: FeatureB.State | |
} | |
enum Action: Equatable { | |
case featureA(FeatureA.Action) | |
case featureB(FeatureB.Action) | |
case task | |
} | |
@Dependency(\.authClient) var authClient | |
var body: some ReducerProtocolOf<Self> { | |
Reduce { state, action in | |
switch action { | |
case .task: | |
print("AppFeature.task") | |
return .run { _ in | |
try await self.authClient.authenticate() | |
} | |
case .featureA, .featureB: | |
return .none | |
} | |
} | |
Scope(state: \.featureA, action: /Action.featureA) { | |
FeatureA() | |
} | |
Scope(state: \.featureB, action: /Action.featureB) { | |
FeatureB() | |
} | |
} | |
} | |
struct AppView: View { | |
var store: StoreOf<AppFeature> | |
var body: some View { | |
VStack { | |
FeatureAView(store: self.store.scope(state: \.featureA, action: AppFeature.Action.featureA)) | |
FeatureBView(store: self.store.scope(state: \.featureB, action: AppFeature.Action.featureB)) | |
} | |
.task { | |
ViewStoreOf<AppFeature>(self.store).send(.task) | |
} | |
} | |
} | |
@main | |
struct TestApp: App { | |
var body: some Scene { | |
WindowGroup { | |
AppView(store: | |
StoreOf<AppFeature>( | |
initialState: AppFeature.State( | |
featureA: FeatureA.State(), | |
featureB: FeatureB.State() | |
), | |
reducer: AppFeature()._printChanges() | |
) | |
) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment