-
-
Save ffried/7bf31121e2a0e494177fa1aa589f27e2 to your computer and use it in GitHub Desktop.
Multi-Subscriber Async Sequence Example
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 SwiftUI | |
actor AccountManager { | |
private var currentUsernameTriggers = Dictionary<UUID, AsyncStream<String?>.Continuation>() | |
private(set) var currentUsername: String? { | |
didSet { | |
for (key, cont) in currentUsernameTriggers { | |
if case .terminated = cont.yield(currentUsername) { | |
print("Continuation with key \(key) was terminated...") | |
removeObserver(forKey: key) | |
} | |
} | |
} | |
} | |
var currentUsernameChanges: AsyncStream<String?> { | |
.init { | |
let key = UUID() | |
currentUsernameTriggers[key] = $0 | |
// If the value rarely changes, but there is a lot of fluctuation in the number of subscribers, | |
// adding this `onTermination` will keep the list of continuations smaller. | |
$0.onTermination = { [weak self] _ in | |
print("Stream with key \(key) was terminated...") | |
guard let self else { return } | |
Task { | |
await self.removeObserver(forKey: key) | |
} | |
} | |
} | |
} | |
private func removeObserver(forKey key: UUID) { | |
currentUsernameTriggers.removeValue(forKey: key) | |
} | |
private static nonisolated let usernameChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
func changeUsername() { | |
currentUsername = String(Self.usernameChars.shuffled().prefix(10)) | |
} | |
} | |
struct ObservingView: View { | |
let accountManager: AccountManager | |
@State | |
private var currentUsername: String? | |
var body: some View { | |
(currentUsername.map(Text.init) ?? Text("No User")) | |
.contentTransition(.interpolate) | |
.animation(.default, value: currentUsername) | |
.task { | |
currentUsername = await accountManager.currentUsername | |
for await username in await accountManager.currentUsernameChanges { | |
currentUsername = username | |
} | |
} | |
} | |
} | |
struct ContentView: View { | |
// Just a little helper to use in SwiftUI's `ForEach` below. | |
// Basically a `UUID` conforming to `Identifiable`. | |
private struct ObserverViewID: Sendable, Hashable, Identifiable { | |
let id = UUID() | |
} | |
private let accountManager = AccountManager() | |
@State | |
private var observerViewIDs = [ObserverViewID(), ObserverViewID()] | |
var body: some View { | |
VStack(spacing: 25) { | |
HStack { | |
Button("Change Username") { Task { await accountManager.changeUsername() } } | |
Button("Add Observer") { observerViewIDs.append(.init()) } | |
} | |
.buttonStyle(.bordered) | |
ScrollView { | |
LazyVStack(spacing: 15) { | |
ForEach(observerViewIDs) { id in | |
HStack { | |
ObservingView(accountManager: accountManager) | |
Spacer() | |
Button(role: .destructive, | |
action: { observerViewIDs.removeAll(where: { $0 == id }) }) { | |
Image(systemName: "trash") | |
} | |
} | |
} | |
} | |
} | |
} | |
.padding() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment