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

Swift6.2にしてアプリをHangさせてしまった話

Avatar for masa38 masa38
November 05, 2025
1.5k

 Swift6.2にしてアプリをHangさせてしまった話

Xcode26/Swift6.2にアップデートした結果、一部の挙動で アプリがカクつきが見られるようになりました。
今回はその原因の調査の過程から解決に至るまでの流れを共有していきたいと思います。

Avatar for masa38

masa38

November 05, 2025
Tweet

Transcript

  1. 今日話したいこと - Xcode26 / Swift6.2 にアップデート後、一部の処理で アプリがHangするよう に なった -

    言語仕様を調査していった結果、挙動変更の背景 を理解できてきたので共有 します
  2. Approachable Concurrencyとは - Swift 6で Strict Concurrency Checking が導入され、データ競合を コンパイル

    時に検出できるようになった - 安全性向上の一方で、Sendable や Actor など扱いが難しく・複雑化 - アプリ開発者がより並行処理を扱いやすくするためのSwiftの設計思想 - まずは単一のメインスレッドで、順番どおりに動く仕組みを理解する。 - その上で async/await、そして並行処理へとステップアップしていくことを提案しています。
  3. Default Actor Isolationとは Approachable Concurrencyを実現するための一つの機能で、デフォルトで安全な スレッド実行を保証する仕組み。 Default Actor IsolationをMainActorに指定することで、コードの多くが自動的に MainActor上で実行。(@MainActor

    を明示しなくてもUI操作は安全に) ※Default Actor Isolationは、Main Threadのほかnonisolated に設定することもできる。この場合、クラス・関数等はデフォ ルトでどのActorにも隔離されない状態になる
  4. struct ThumbnailView: View { @Environment(¥.displayScale) private var displayScale: CGFloat let

    imageFile: ImageFile @State private var loadedThumbnail: Image? var body: some View { content .task(id: displayScale) { guard let thumbnail = await imageFile.makeThumbnail(displayScale: displayScale) else { loadedThumbnail = Image(systemName: "x.square") return } loadedThumbnail = Image(uiImage: thumbnail) } } @ViewBuilder var content: some View { … 該当のコードを見てみる(ThumbnailView) サムネイル画像の生成処理 (MainActorで実行する)
  5. struct ImageFile: Identifiable { let fileURL: URL static let maxThumbnailHeight:

    CGFloat = 50 … // サムネイル画像の生成 func makeThumbnail(displayScale: CGFloat) async -> UIImage? { guard let image else { return nil } return await resizeImage(image, maxThumbnailSize: Self.maxThumbnailHeight, displayScale: displayScale) } // 画像のリサイズ private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat, displayScale: CGFloat) async -> UIImage? { let originalSize = image.size let maxDimension = max(originalSize.width, originalSize.height) let shrinkFactor = maxThumbnailSize / maxDimension let newSize = CGSize( width: originalSize.width * shrinkFactor * displayScale, height: originalSize.height * shrinkFactor * displayScale ) return image.preparingThumbnail(of: newSize) } } 該当のコードを見てみる(ImageFile) @MainActorが暗黙的に有効に どのActorにも隔離されていない nonisolatedなクラス バックグラウンド スレッドでの実行 (nonisolated async) メインスレッドス レッドで実行され るように・・
  6. // 画像のリサイズ nonisolated private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat,

    displayScale: CGFloat) async -> UIImage? { … } コードを変更してみる nonisolatedを明示しても、 メインスレッドで実行され てしまった・・
  7. nonisolated func syncNonIsolated() { ... } nonisolated func asyncNonIsolated(data: SendableType)

    async { ... } // Swift 6.2で追加 nonisolated(nonsending) func asyncNonIsolatedNonsending(data: NonSendableType) async { ... } Swift6.2のnonisolatedな関数の振る舞いの変化 呼び出し元のActor/スレッドで 実行される(変更無し) New 呼び出し元のActor/スレッドで 実行される(※2) (Actorを跨ぐ必要がなくなったので、非 Sendable(=nonsending)な型を扱える) ※1. SE-0338 「アクター分離されていない非同期関数の実行について」 ※2. SE-0461 (A nonisolated(nonsending) async function does not hop to another actor, so its arguments and results do not need to be Sendable.) (変更後) nonisolated(nosending) By Default の設定に依存 (変更前) 呼び出し元のActor 外での実行 (※1)
  8. 以前のnonisolated asyncを再現するには? - Swift6.2で追加された @concurrent を付与する - Swift6.1以前のnonisolated asyncの挙動と同様、呼び出し元Actorとは独立し て並行実行される

    - @concurrent 属性を付与した場合、その関数は nonisolated の扱いになる。 - 下記例のように、Actor隔離された状態にアクセスできない
  9. まとめ - Swift 6.2 の Approachable Concurrency は、アプリ開発者が 並行処理を学ぶた めのハードルを下げる

    ことを目的とした設計思想 - 今回の例では 仕様の変更点を正しく理解できていなかった ことで、結果的 に アプリをハングさせてしまいました。 - Instruments でボトルネックを可視化し、@concurrent を用いて安全に並列化 することで解消しました。 - 自社プロダクトにも仕様を良く理解したうえで、慎重に導入検討していきた い と思いました。