Replace Combine with @Observable and AsyncStream#6
Open
g-enius wants to merge 49 commits intofeature/navigation-stackfrom
Open
Replace Combine with @Observable and AsyncStream#6g-enius wants to merge 49 commits intofeature/navigation-stackfrom
g-enius wants to merge 49 commits intofeature/navigation-stackfrom
Conversation
g-enius
commented
Mar 2, 2026
a5a9a1e to
0257d83
Compare
|
Claude finished @g-enius's task in 2m 40s —— View job Creating Git Tags for Each BranchFollowing the existing
All 3 tags are now live on the remote. You can view them at: |
3a7a545 to
dcc291b
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…OS 17+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
subscribeToDarkMode() now reads the initial value from the service before subscribing to the stream. Removed the registration-wait pattern (observeDarkMode) which missed the initial registration due to Task starting after the synchronous yield. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ster Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single observeServiceRegistrations() dispatches to subscribeToToasts() and subscribeToDarkMode() on registration events. Removes explicit subscribe calls from flow transitions — activateSession triggers re-registration which the watcher picks up automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both sessions register toast, so the registration watcher auto-resubscribes via subscribeToToasts() — same as dark mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The AsyncSequence migration is done — these comments described what the code already does and referenced the wrong iOS version. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tDescription NavigationStack maturity and Symbol effects are not specific to the AsyncSequence/Observable migration — only @observable and @bindable are. Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
- Remove 2 non-AsyncSequence-specific iOS 17 bullets from deploymentTargetDescription (NavigationStack maturity and symbol effects do not belong in async-sequence branch) - Restore DispatchGroup callback example from navigation-stack branch — it was rewritten unnecessarily; the callback pattern is branch-agnostic Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
…base branch Option 3 (async/await TaskGroup) is branch-agnostic — the TaskGroup pattern doesn't change between Combine and AsyncSequence. Restored the order-preserving version from feature/navigation-stack exactly. Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
- TechnologyItem enum: case combine → asyncSequence (raw value 'asyncsequence') - TechnologyDescriptions: combineDescription → asyncSequenceDescription - FeaturedItem: static let combine → asyncSequence, update carouselSet1 - TechnologyDescriptionsTests: update test name, enum ref, and assertion - DetailViewModelTests: .combine → .asyncSequence - DeepLinkTests: update test URL to use new raw value Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
…pattern in combineDescription Shows how didSet + Task.sleep replaces Combine's .debounce operator. Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
…escription Use the same (Int, [Item]) tuple structure and sorted-flatMap order preservation as the TaskGroup example — only the concurrency primitive differs. Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 'Before (Combine)' example used an improved version with [weak self]
and a query parameter, which is unrelated to the AsyncSequence migration.
Per PR discipline, this change belongs on the base branch first. Reverted
to match feature/navigation-stack exactly: .sink { self.performSearch() }
Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
- favoritesChanges → favoritesStream (FavoritesServiceProtocol + impl + mock + PreviewHelper + ViewModels + tests) - featuredCarouselChanges → featuredCarouselStream (FeatureToggleServiceProtocol + impl + mock + PreviewHelper + HomeViewModel + TechnologyDescriptions + tests) - appearanceModeChanges → appearanceModeStream (FeatureToggleServiceProtocol + impl + mock + PreviewHelper + AppCoordinator + tests) Naming mirrors the Combine convention (e.g. toastPublisher → toastStream) and is consistent with toastStream renamed in a prior commit. The *Stream suffix names the mechanism uniformly regardless of whether the stream carries "changes" or "events". Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AsyncStream.makeStream(of:) registers the continuation immediately instead of lazily when iteration starts. Values yielded before the consumer starts iterating are now buffered correctly. Service tests use direct iterator (no spawned Task needed). ViewModel tests reduced from 50ms to 10ms sleep. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ItemsView never uses $viewModel.xxx — it only passes viewModel to ItemsMainContent. @bindable is correctly placed on ItemsMainContent, which does use $viewModel.searchText. Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
Co-authored-by: Charles Wang <g-enius@users.noreply.github.com>
@observable and AsyncStream.makeStream(of:) both require iOS 17 independently — AsyncSequence itself is iOS 13. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The branch name was misleading — AsyncSequence/AsyncStream are iOS 13 APIs. The actual iOS 17 drivers are @observable and AsyncStream.makeStream(of:). Updated all references across 8 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
didSet fires even when the value doesn't change (e.g. keyboard dismiss re-sets the same text). Guard with oldValue check — equivalent to .removeDuplicates() in the Combine pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add guard != oldValue on didSet blocks that yield to StreamBroadcaster (prevents redundant stream events, mirrors Combine's removeDuplicates) - Replace Task.sleep with deterministic awaitObservation helper using withObservationTracking + withCheckedContinuation - Fix DetailViewModelTests double-registration and use viewModel action - Fix SwiftLint line length in HomeViewModel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0257d83 to
7545fc6
Compare
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces all Combine usage with two independent iOS 17 technologies —
@Observable(Observation framework) for ViewModel → View reactivity, andAsyncStream+ customStreamBroadcasterfor service event streams — achieving zeroimport Combineacross the entire codebase.import Combineremaining: 0Depends on: PR #3 — Migrate UIKit navigation to SwiftUI NavigationStack
Why iOS 17?
AsyncSequence/AsyncStreamare iOS 13 APIs. The iOS 17 requirement comes from:@ObservableObservableObject+@PublishedAsyncStream.makeStream(of:)(SE-0388)AsyncStream { continuation in }StreamBroadcasterNeither depends on the other; they coincidentally share the same deployment target.
Key Changes
Reactive State
AnyPublisher<Set<String>, Never>AsyncStream<Set<String>>CurrentValueSubject/PassthroughSubjectStreamBroadcaster(custom, in Core).sink { }.store(in: &cancellables)Task { for await value in stream { } }Set<AnyCancellable>cleanuptask.cancel().debounce(for:scheduler:)didSet+Task.sleepwith cancellationViewModel Observation
ObservableObject+@Published@Observablemacro@ObservedObject@Bindable(two-way) or plain property@StateObject@StateStreamBroadcaster
Custom
@MainActor-isolated multi-consumer broadcaster replacing Combine subjects. Each consumer gets an independentAsyncStream— cleanup is automatic on Task cancellation.What Stays the Same
Migration Pitfalls
1.
guard let selfbeforefor await= retain cycle. The strong capture persists across the entire loop suspension.2. AsyncStream doesn't emit current value on subscribe. Unlike
@Published, must read current state at init time, then observe future changes via stream.3.
@Observableconflicts with property wrappers. Service properties need@ObservationIgnored @Service(.network).4.
@StateObjectrequiresObservableObject. Use@Statefor ownership with@Observableclasses.Test plan
import Combinestatements🤖 Generated with Claude Code