Skip to content

Instantly share code, notes, and snippets.

@gohanlon
Created November 9, 2022 23:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gohanlon/107850c1da7e963b40105a8b2c44ebfc to your computer and use it in GitHub Desktop.
Save gohanlon/107850c1da7e963b40105a8b2c44ebfc to your computer and use it in GitHub Desktop.
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