Replace Combine with @Observable and AsyncStream#6
Open
g-enius wants to merge 50 commits intofeature/navigation-stackfrom
Open
Replace Combine with @Observable and AsyncStream#6g-enius wants to merge 50 commits intofeature/navigation-stackfrom
g-enius wants to merge 50 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
0257d83 to
7545fc6
Compare
dcc291b to
2bd34b7
Compare
639e05a to
532c2b7
Compare
2bd34b7 to
2646967
Compare
532c2b7 to
e879b9b
Compare
2646967 to
6adccda
Compare
dd52906 to
dcbf6df
Compare
4483f42 to
5d8fd4e
Compare
2830a34 to
2b0dad1
Compare
8 tasks
7c1d18a to
0f7b924
Compare
9fb54c0 to
558628b
Compare
0f7b924 to
a92a023
Compare
621bee6 to
5c3a497
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>
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 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>
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>
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>
Restore @bindable on views using $viewModel bindings (HomeView, ItemsView). Use @ObservedObject for SettingsView (ObservableObject VM). Resolve pre-existing conflict markers in DetailViewModelTests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[skip ci] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a92a023 to
8545aa6
Compare
…based DI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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@StateDependency Injection
@Serviceproperties require@ObservationIgnoredsince@Observablecan't observe property wrappers with customstatic subscriptAppCoordinatorconforms toServiceLocatorProviderwith@ObservationIgnored public let serviceLocatorServiceLocatorProviderwith@ObservationIgnoredonserviceLocatorand@ServicepropertiesserviceLocator.reset()— live views may still resolve services via@Serviceduring SwiftUI teardown; the next session'sactivate()overwrites with fresh instancesStreamBroadcaster
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