Skip to content

Feature/xcode 26#233

Open
YutoMizutani wants to merge 2 commits into
developfrom
feature/xcode-26
Open

Feature/xcode 26#233
YutoMizutani wants to merge 2 commits into
developfrom
feature/xcode-26

Conversation

@YutoMizutani

@YutoMizutani YutoMizutani commented Mar 18, 2026

Copy link
Copy Markdown
Owner

OperantKit Xcode 26 / RxSwift 6 マイグレーションノート

概要

feature/xcode-26 ブランチでは、OperantKit を RxSwift 4.x → 6.x へ移行し、Swift Package Manager の定義を swift-tools-version 4.2 → 5.9 に更新する作業を行った。


変更一覧

1. Package.swift の更新

項目 Before After
swift-tools-version 4.2 5.9
platforms 未指定 iOS 13, macOS 10.15, tvOS 13, watchOS 6
RxSwift バージョン "4.0.0" ..< "5.0.0" exact: "6.10.2"
dependency 構文 "RxSwift" 文字列指定 .product(name:package:) 形式
ターゲットパス "Sources" / "Tests" "OperantKit" / "OperantKitTests"
swiftLanguageVersions 未指定 [.v5]

2. RxSwift API の変更 (.share(replay:).share(replay:scope:))

RxSwift 6 では share(replay:) のデフォルト scope が変わったため、明示的に .whileConnected を指定する必要がある。

対象ファイル:

  • Sources/Application/Extensions/ObservableType+.swift
  • Sources/Application/Extensions/UIApplication+Rx.swift
  • Sources/Application/Extensions/UIViewController+Rx.swift
  • Examples/iOS/RatChamber/.../SessionPresenter.swift
// Before (RxSwift 4)
.share(replay: 1)

// After (RxSwift 6)
.share(replay: 1, scope: .whileConnected)

3. ObservableType+.swift のジェネリクス型変更

// Before (RxSwift 4/5)
E

// After (RxSwift 6)
Element

RxSwift 6 では ObservableType の associated type エイリアス E が廃止され、Element に統一された。store メソッド等の型パラメータ表記を EElement に変更。

対象ファイル:

  • Sources/Application/Extensions/ObservableType+.swift

4. Single.create クロージャの .error().failure() 変更

RxSwift 6 では SingleEvent が Swift 標準の Result<T, Error> に変更された。これにより single(.error(...))single(.failure(...)) に変更が必要。

// Before (RxSwift 4/5)
single(.error(RxError.noElements))

// After (RxSwift 6)
single(.failure(RxError.noElements))

対象ファイル(計 6 ファイル、41 箇所):

  • Sources/Application/Protocols/Repositories/ScheduleRepository.swift (14 箇所)
  • Sources/Domain/UseCases/Timer/CADisplayLinkTimerUseCase.swift (6 箇所) ← 修正漏れにより後から追加
  • Sources/Domain/UseCases/Timer/CVDisplayLinkTimerUseCase.swift (6 箇所)
  • Sources/Domain/UseCases/Timer/WhileLoopTimerUseCase.swift (6 箇所)
  • Sources/Domain/UseCases/Timer/StepTimerUseCaseTimerUseCase.swift (6 箇所)
  • Sources/Domain/UseCases/DiscreteTrialUseCase.swift (3 箇所)

5. ResponseEntityclass のまま維持

当初 classstruct への変更を試みたが、コードベース全体(54 ファイル)が do(onNext:) クロージャ内での共有オブジェクト変更など参照セマンティクスに依存しているため、class に戻した。

// 維持
public class ResponseEntity: Responsible { ... }

struct に変更する場合は、Rx パイプライン全体のアーキテクチャ変更が必要。

6. .gitignore の更新

  • .swiftpm/ ディレクトリを追加(Xcode の SPM メタデータを除外)
  • vendor ディレクトリを追加(Ruby bundler 用)

7. Cartfile.resolved の更新

  • RxSwift: 5.0.16.10.2

8. Gemfile.lock の更新

  • fastlane: 2.123.02.231.1
  • xcodeproj: 1.9.01.27.0
  • bundler: 2.0.12.3.7
  • その他多数の gem が更新

9. RatChamber (Example App) の Carthage → SPM 移行

RatChamber は Carthage で ../../../Carthage/Build/iOS/ からフレームワークをリンクしていたが、Carthage/Build/iOS ディレクトリが存在しないためリンクエラーが発生。

変更内容:

  • Presenter.swift: protocol Presenter: classprotocol Presenter: AnyObject
  • RatChamber.xcodeproj/project.pbxproj:
    • Carthage フレームワーク参照(RxCocoa.framework, RxSwift.framework, RxRelay.framework, OperantKit.framework)を削除
    • CopyFiles (Embed Frameworks) ビルドフェーズをクリア
    • FRAMEWORK_SEARCH_PATHS から $(PROJECT_DIR)/../../../Carthage/Build/iOS を削除
    • SPM パッケージ依存として RxSwift 6.10.2 (exact) を追加
    • RxSwift, RxCocoa, RxRelay の XCSwiftPackageProductDependency をリンク
    • OTHER_LDFLAGS-framework OperantKit を追加(ワークスペース内のフレームワークターゲット)

10. Makefile の Carthage → SPM 移行

項目 Before After
CARTHAGE_COMMAND carthage 削除
install-frameworks carthage bootstrap --no-use-binaries xcodebuild -resolvePackageDependencies
update-frameworks carthage update --no-use-binaries xcodebuild -resolvePackageDependencies
build-all carthage build + framework ビルド framework ビルドのみ

つまづきポイント・人間が気づきにくい箇所

.share(replay:) の scope 問題

RxSwift 6 で share(replay:) を呼ぶと、コンパイルは通る場合があるが、デフォルト scope が .forever に変更されている。これにより、購読者がいなくなっても内部バッファがリセットされず、メモリリークや古い値の再送が発生する。明示的に scope: .whileConnected を付けないと、RxSwift 4 時代と異なる挙動になる。

コンパイルエラーにならないため、テストやレビューで見落としやすい。プロジェクト全体を .share(replay: で grep して網羅的に確認する必要がある。

EElement の型パラメータ変更

RxSwift 6 で ObservableType の associated type エイリアス E が廃止された。E のままだとコンパイルエラーになる。エラーメッセージは「Cannot find type 'E' in scope」で出るため、RxSwift のバージョン差異が原因だと気づくのに時間がかかる。プロジェクト全体を E で検索しても一般的な識別子であるため、grep でのフィルタリングが困難。

SingleEventResult 型への変更

RxSwift 6 では SingleEventResult<T, Error> に変更された。.error().failure().success() はそのまま。エラーメッセージは Type 'Result<..., any Error>' has no member 'error' と出るが、「Result 型」と「RxSwift の Single API 変更」が結びつかず、原因特定に時間がかかる。特に Single.create のクロージャが多数あるプロジェクトでは修正漏れが起きやすい。

#if os(iOS) によるプラットフォーム条件付きコンパイルの修正漏れ

CADisplayLinkTimerUseCase.swift#if os(iOS) || os(tvOS) で囲まれており、macOS ターゲットでビルドするとコンパイル対象外になるため、エラーが検出されない。実際に、他の Timer ファイル(WhileLoopTimerUseCaseCVDisplayLinkTimerUseCaseStepTimerUseCase)はすべて .failure() に修正済みだったにもかかわらず、CADisplayLinkTimerUseCase だけが .error() のまま残存していた。iOS ターゲットでビルドして初めてエラーが顕在化する。プラットフォーム条件付きファイルは grep ベースの一括置換で対応すべき。

ResponseEntity の class → struct 変更の危険性

値型に変更すると、既存コードで 変数に代入して共有していた箇所が、コピーセマンティクスに変わる。Rx の do(onNext:) クロージャ内で ResponseEntity のプロパティを変更する副作用パターンが多数あり、struct にすると 'entity' is a 'let' constant エラーが発生する。仮に var にしても、クロージャ内でのコピーへの変更は元のオブジェクトに反映されないため、無音でバグになる。この型は class のまま維持する必要がある。

Package.swift のターゲットパス変更

パスを "Sources""OperantKit" に変更しているが、実際のファイルシステム上のディレクトリ構造が対応しているかの確認が必要。SPM はターゲットパスと実ディレクトリが一致しないとビルドに失敗するが、エラーメッセージが分かりにくい("no sources found" 等)。setup-structure.sh はこの問題に対応するために用意されたと思われる。

RatChamber の Carthage → SPM 移行時のリンクエラー

RatChamber は ../../../Carthage/Build/iOS/FRAMEWORK_SEARCH_PATHS に指定して Carthage ビルド済みフレームワークを参照していた。Carthage を削除した後、単純にフレームワーク参照を削除するだけでは コンパイルは通るがリンクで失敗する。SPM パッケージは iOS ターゲットではフレームワークバンドル(.framework)ではなくオブジェクトファイル(.o)として BUILT_PRODUCTS_DIR に配置されるため、-framework RxSwift でリンクしても見つからない。RatChamber 自身のプロジェクトにも XCRemoteSwiftPackageReferenceXCSwiftPackageProductDependency を追加し、SPM が正しくリンクを構成するようにする必要がある。

RxSwift の exact バージョン指定

.package(url: "https://github.com/ReactiveX/RxSwift.git", exact: "6.10.2")

exact 指定は将来のパッチアップデートも受け取れなくなる。意図的なピン留めであれば問題ないが、通常は .upToNextMinor(from:).upToNextMajor(from:) の方が保守しやすい。


エージェントの洞察

型推論の不一致: Result 型と RxSwift の SingleEvent

RxSwift 6 で SingleEventResult<Element, Swift.Error> の typealias に変更されたことにより、コンパイラのエラーメッセージが Type 'Result<Void, any Error>' has no member 'error' と表示される。これは Swift 標準ライブラリの Result 型のエラーであり、RxSwift の API 変更だと認識しづらい。特に .success() はそのまま動くため、.error() だけが壊れる非対称性があり、部分的にコンパイルが通る状態になりやすい。

暗黙的なメモリリスク: ResponseEntity の参照セマンティクス依存

ResponseEntityclass として維持する必要があるが、これは Rx パイプライン内で do(onNext:) を使った副作用パターン(外部オブジェクトのプロパティ変更)に全面的に依存しているためである。このパターンには以下のリスクがある:

  1. 循環参照: do(onNext:) クロージャが ResponseEntity インスタンスを強参照で保持し、そのインスタンスが Observable チェーンを保持する場合、循環参照が発生する。現状のコードでは ResponseEntity は Observable を保持していないため直接的な問題はないが、将来の拡張時に注意が必要。

  2. スレッドセーフティ: ResponseEntity のプロパティ(numOfResponses, milliseconds)が複数の Rx ストリームから同時に変更される可能性がある。class のプロパティへの同時書き込みはデータレースになる。WhileLoopTimerUseCaseDispatchQueue を使用している箇所があり、マルチスレッド環境での使用が想定されている。

  3. Observable チェーン内での副作用の順序保証: do(onNext:) 内での変更は、後続の map/flatMap に即座に反映されるが、複数の do(onNext:) が異なるストリームで同じ ResponseEntity を変更する場合、変更順序は購読タイミングに依存する。

ObservableType.E の廃止パターン

E は RxSwift 4/5 時代に ObservableType.Element の短縮エイリアスとして存在したが、Swift のジェネリクスシステムの改善に伴い RxSwift 6 で廃止された。エラーメッセージ Cannot find type 'E' in scope は一見するとローカルなジェネリクスパラメータの問題に見え、RxSwift のマイグレーションガイドを参照しないと原因特定が困難。同様のエイリアス廃止が他の RxSwift プロトコル(ObserverType 等)でも発生する可能性がある。

プラットフォーム条件付きコンパイル (#if os(...)) による修正の「死角」

本マイグレーションで最も見落としやすかったのが、#if os(iOS) || os(tvOS) で囲まれた CADisplayLinkTimerUseCase.swift の修正漏れである。macOS ターゲットでのビルドでは当該ファイルがコンパイル対象外となり、エラーが一切検出されない。同じディレクトリ内の他3ファイル(CVDisplayLinkTimerUseCaseWhileLoopTimerUseCaseStepTimerUseCase)はすべて修正済みであったため、ディレクトリ単位での確認では「完了済み」と誤認しやすい。マイグレーション作業では 全プラットフォームでのビルド、または grep による機械的な一括置換 が不可欠である。IDEのビルドエラーだけに頼ると、条件付きコンパイルブロック内の未修正コードが残存するリスクがある。

Carthage → SPM 移行時の「コンパイル成功・リンク失敗」の罠

Carthage から SPM への移行時、ワークスペース内で SPM パッケージが解決されていれば コンパイルは成功する(モジュールマップが BUILT_PRODUCTS_DIR から解決される)。しかし、リンクフェーズで Carthage パスの .framework を探しに行き ld: framework 'RxCocoa' not found で失敗する。コンパイルが通るため「コード修正は完了」と誤認しやすいが、実際にはプロジェクトの依存管理の書き換えが未完了。SPM では各ターゲットに XCSwiftPackageProductDependency を明示的に追加する必要があり、ワークスペース内で共有されている SPM パッケージは自動的にはサブプロジェクトにリンクされない。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant