Upgrade to Pro — share decks privately, control downloads, hide ads and more …

是時候了!將RxSwift 程式碼全部改寫成 Swift Concurrency吧!/slid...

是時候了!將RxSwift 程式碼全部改寫成 Swift Concurrency吧!/slides_iplayground_2026_20min_static

「iPlayground 2026」の登壇資料です。
https://iplayground.io/2026/

More Decks by LINE Digital Frontier - TECH

Other Decks in Technology

Transcript

  1. ) 10 10 LINE Digital Frontier LINE Digital Frontier LINE

    LY Corp. LINE LY Corpration UI iOSDC Japan iOSDC Japan VT 🏍💨 人 日 身 5 2 日 0 2 車 ・ 5 2 0 2 面 ・ ・ VTuber 己 自 自 Dex( @dsxsxsxs
  2. ViewModel ViewModel 8 flatMapLatest • •8 ViewModel disposeBag disposeBag ViewModel

    BehaviorRelay disposed(by:) BehaviorRelay flatMapLatest Rx • 3 • Rx 2026 • ・ 入 一 一 ⾒ ・ 見 二 見 一 AI 2026 • 日 disposed(by:) AI
  3. 2026 Rx 2026 • 2015 Rx • 2015 ⾒ Rx

    ⾒ • • Combine UI @Observable • async/await • 2026 AsyncSequence Combine @Observable • Rx . async/await AsyncSequence . • Swift Async Algorithms 2025 buffer replay 日 share / buffer 穴 入 ・ 非 一 ・ ・ 一 Swift Async Algorithms 一 ・ ・ 6.10.2 子 非 2 0 1 6 6 2 • Rx share replay • 0 2 Rx
  4. AI plan mode • plan mode • • Prompt •

    codebase 查 RxSwift Swift Concurrency • Combine tasks • • task(.md) PR md AI plan mode feature flag tasks task 1 PR flag 行 3 工 月 人 一 支 一 RxSwift 月 • codebase 3 ・ 9 ・ 1 • 行 • 人 用 工 AI
  5. AI AI • plan mode • plan mode • Prompt

    • Wave 查 rule rule • rule 用 rule 一 入 wave 一 •
  6. rules rules wave wave actor • wave • rules Combine

    rules Rx async Rx state vs event actor / Combine async state / event • rules • rules wave rules 用 十 一 用 手 用 AI 一 一 一 rules • AI 土 •
  7. & Combine Single<T> async throws -> T Future<T?, Error> Observable<T>

    AsyncSequence<T> AnyPublisher<T, Error> BehaviorRelay<T> AsyncStream+share(history:1) CurrentValueSubject PublishRelay<T> AsyncStream+share() PassthroughSubject DisposeBag Task Set<AnyCancellable> flatMapLatest flatMapLatest (iOS18+) map { }.switchToLatest() do(onNext:) 無 handleEvents(receiveOutput:) 一 Swift Concurrency 一 RxSwift
  8. UIKit binding & UIKit binding RxSwift Swift Concurrency Combine withLatestFrom

    combineLatest+ 取最新(無直接對應) share(replay:1) share(history:1)(polyfill) .rx.tap / .rx.text UIAction / addTarget(.editingChanged) collectionView.rx.didScroll publisher(for: \.contentOffset)(KVO) share() (沒有replay) 族繁不及備載,完整對照見 RxSwift Combine Cheatsheet(CombineCommunity) 與 Swift Async Algorithms Docs → Combine Cheatsheet 子 RxSwift Async Algorithms Docs CombineCommunity Swift
  9. 60 • • 60 rules /9 / 9 wave 2

    3 4 4 PR task task PR md • • skill 2 3 md • AI rules skill wave • AI Skill 1 skill /f-rxswift E-03 支 一 – 一 支 月 一 月 手 ・ 支 人 一 一 ・ 一 一 一 行 (skill を呼び出す prompt) 行 人 md md
  10. skill / rules 1 wave • skill = rules =

    • skill = rules = task / wave • task / wave • lessons lessons skill rules skill / rules 回顧這次 E-03 學到的 lessons & takeaways,適當反映到 f-rxswift 的 skill、rules 或各 task md E-03 で得た lessons と takeaways を振り返り、必要に応じて f-rxswift の skill / rules、または各 task md に反映してください。 guideline Review Bot PR 人 AI reviewer CI 一 一 一 走 review guideline CI 生 人 人 用 一 手 • F-RxSwift Review Guideline 示 F-RxSwift PR • 人 面 skill / rules guideline
  11. repository.fetch(id: "42") .subscribe(onSuccess: { render($0) }, onFailure: { show($0) })

    .disposed(by: disposeBag) task = Task { do { let result = try await repository.fetch(id: "42") Task.checkCancellation() render(result) } catch { CancellationError {} catch { show(error) } } deinit { task?.cancel() }
  12. async async Single Single func fetch(id: String) -> Single<Profile> {

    Single.create { observer in let task = Task { do { observer(.success(try await repository.fetch(id: id))) } catch { observer(.failure(error)) } } return Disposables.create { task.cancel() } } } • 讓尚未重構的 Rx 下游仍可沿用 Single ,底層其實已是 async 未移行の Rx 利用側はそのまま Single を使えるが、内部はすでに async • 關鍵: Disposables.create { task.cancel() } 將 Rx 的 dispose 接回 Task 取消 要点:Disposables.create で task.cancel() を呼び、Rx の dispose を Task キャンセルへつなぐ
  13. Rx 話 Rx Before:Rx 滲透內層 After:Rx 僅存最外圈 UI UI ・Rx

    UI ・Rx Domain ・Rx Domain ✓ RxSwift package Infra ・Rx • Infra • Infra Infra ✓ Infrastructure Infrastructure RxSwift package Data Layer Data Layer • Clean Domain / Infra 見 Rx 日 • Clean Architecture Rx
  14. Observable Observable • Repository • Repository AsyncStream AsyncStream UseCase UseCase

    VM VM View View AsyncStream AsyncStream •Observable → AsyncSequence(Domain)/ AnyPublisher(UI); .accept() → continuation.yield() • Observable AsyncShareSequence / AnyPublisher • AsyncSequence • AsyncSequence • continuation.yield() multicast / replay multicast / replay multicast • multicast .accept() API flatMap cold API share() share() Rx 的 hot stream 無法 1:1 對應 AsyncSequence ── 需以 share,或自行補上 replay。 支 一 生 行 用 方 方 • Rx の hot stream は、AsyncSequence へ 1:1 では移せない。share を使うか、replay を補う必要がある。
  15. UseCase UseCase 生 final class PageListUseCase { private let trigger

    = PublishRelay<Void>() let state: Observable<Page> init(repo: Repo) { state = trigger .flatMapLatest { repo.fetch().asObservable() } .share(replay: 1, scope: .forever) } func reload() { trigger.accept(()) } }
  16. UseCase UseCase final class PageListUseCase: PageListUseCase { private let (reloads,

    reloadCont) = AsyncStream<Void>.makeStream() let state: AsyncShareSequence<Result<Page, DemoError>> init(repo: Repo) { state = reloads .flatMapLatest { resultStream { try await repo.fetch() } } .share(history: 1) } func reload() { reloadCont.yield(()) } 生 }
  17. UseCase UseCase final class PageListUseCase { private var task: Task<Void,

    Never>? func reload() { task?.cancel() task = Task { do { continuation.yield(.success(try await repo.fetch())) } catch is CancellationError { // The request was replaced by a newer reload. } catch { continuation.yield(.failure(.failed(String(describing: error)))) } } } deinit { task?.cancel() } 生 }
  18. ViewModel ViewModel 面 力 面 final class ItemListViewModel { private

    let bag = DisposeBag() let items: Driver<[Row]> init(useCase: PageListUseCase) { items = useCase.state .compactMap { $0.value?.rows } .asDriver(onErrorDriveWith: .empty()) } }
  19. ViewState ViewState @MainActor final class PageListViewState: ObservableObject { @Published private(set)

    var items: [Row] = [] private var task: Task<Void, Never>? init(useCase: PageListUseCase) { task = Task { @MainActor [weak self, useCase] in for await result in useCase.state { guard let self else { return } items = result.page?.rows ?? items } @Observable } @Observable } deinit { task?.cancel() } 面 力 面 }
  20. polyfill polyfill // iOS 18+:官方多訂閱 —— 但沒有 replay import AsyncAlgorithms

    let shared = stream.share() // multicast、無回放 // 任一 iOS,且需回放最新值(狀態流):本專案 polyfill let state = stream.share(history: 1) // multicast + 回放最新 = Rx share(replay:1) replay 自 行 自 replay 自 share 穴 8 8 1 iOS share 自 iOS 1 一 replay replay
  21. polyfill / polyfill / AsyncShareSequence AsyncShareSequence + replay + @available(iOS,

    deprecated: 18.0) public final class AsyncShareSequence<Element: Sendable>: AsyncSequence, Sendable { private func subscribe() -> AsyncStream<Element> { ... } // 同步註冊 + replay init(base: AsyncStream<Element>, history: Int = 0) { ... } public func makeAsyncIterator() -> Iterator { Iterator(base: subscribe().makeAsyncIterator()) } } extension AsyncSequence where Self: Sendable, Element: Sendable { public func share(history: Int = 0) -> AsyncShareSequence<Element> { AsyncShareSequence(base: self, history: history) } } 使用端只看見 share(history:);狀態流固定用 history: 1。 一 一 4 1 4 1 利用側に見えるのは share(history:) だけ。状態ストリームには history: 1 を使う。
  22. polyfill / polyfill AsyncShareSequence AsyncShareSequence private struct State { var

    subscribers: [Int: AsyncStream<Element>.Continuation] = [:] var history: [Element] = [] let historyCount: Int mutating func push(_ value: Element) { history.append(value) lock 內只更新 history、取得 history = Array(history.suffix(historyCount)) 訂閱者快照; } 不在 lock 內呼叫下游。 } lock 内では history の更新と購読者の private let state: OSAllocatedUnfairLock<State> private let iterationTask: Task<Void, Never> 二 二 4 2 public func subscribe() -> AsyncStream<Element> {} snapshot 取得だけを行い、 下流は呼び出さない。
  23. polyfill / polyfill / 一 片 → 4 一 3

    4 3 iterationTask = Task { [state] in broadcast 前檢查 cancellation; defer { Self.finishSubscribers(in: state) } 所有離開路徑都由 defer 清理。 do { 配信前に cancellation を確認し、どの終了経路で for try await value in base { も defer で後片付けする。 try Task.checkCancellation() state.withLock { $0.push(value) $0.subscribers.forEach { $0.yield(value) } } } } catch is CancellationError where Task.isCancelled { // defer cleanup } catch { // unexpected throw normal finish } }
  24. polyfill / polyfill / replay replay 4 4 4 4

    private func subscribe() -> AsyncStream<Element> { let (stream, cont) = AsyncStream<Element>.makeStream() let id = UUID() state.withLock { $0.history.forEach { cont.yield($0) } $0.subscribers[id] = cont } cont.onTermination = { [state] _ in state.withLock { $0.subscribers[id] = nil } } return stream replay 與註冊放在同一個 critical section,避免兩者之間漏值 } replay と購読者登録を同じ critical section で行い、その間の取りこぼしを防ぐ
  25. Single<Response> Single<Response>; 3 ×150 APIClient • A. client async client

    • B. async client • B. async async • C. async • C. async client core core Single 用 Single 生 立 行 • A. 行 RxSwift shim 而 •
  26. 150 150 caller extension APIRequest { var asAsync: T {

    get async throws { try await execute() } } func asSingle() -> Single<T> { Single.create { observer in let task = Task { do { observer(.success(try await self.asAsync)) } catch { observer(.failure(error)) } } return Disposables.create { task.cancel() } } } 一 }
  27. 150 caller 150 • APIRequest.asAsync • APIRequest.asAsync API .asSingle 150

    Single.create .asAsync Rx 150 shim 150 一 生 一 行 一 行 150 • Single.create shim shim Rx • 人 .asSingle .asSingle 150 caller • .asSingle caller 143 .asAsync await .asAsync • 143 • 150 一 • API
  28. 3 • 1. async async • • 2. API /

    API Domain UseCase Domain UseCase Combine • Combine UI binding / UI binding replay replay SwiftUI SwiftUI • 3. • 第三條的真意:能省一事就省一事,不把時間浪費在短命的重構上。 ・ 面 面 ・ 一 ・ 支 ・ 一 ・ 止 走 3 つ目の趣旨は、やらずに済むことはやらないこと。すぐ捨てられるリファクタリングに時間を使わない。