Skip to content

Instantly share code, notes, and snippets.

@ffried
Last active September 27, 2024 11:45
Show Gist options
  • Save ffried/7bf31121e2a0e494177fa1aa589f27e2 to your computer and use it in GitHub Desktop.
Save ffried/7bf31121e2a0e494177fa1aa589f27e2 to your computer and use it in GitHub Desktop.
Multi-Subscriber Async Sequence Example
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