Slide 1

Slide 1 text

Swift 6.2にしたらアプリを Hangさせてしまった話

Slide 2

Slide 2 text

自己紹介 宮川 昌高(ミヤガワ マサタカ) - 2024年12月にディップ株式会社に中途入社 - 入社後はFlutterでのアプリ開発を担当 - 6月から担当するプロダクトが変わりiOSエンジニアになりました!

Slide 3

Slide 3 text

今日話したいこと - Xcode26 / Swift6.2 にアップデート後、一部の処理で アプリがHangするよう に なった - 言語仕様を調査していった結果、挙動変更の背景 を理解できてきたので共有 します

Slide 4

Slide 4 text

Approachable Concurrencyとは - Swift 6で Strict Concurrency Checking が導入され、データ競合を コンパイル 時に検出できるようになった - 安全性向上の一方で、Sendable や Actor など扱いが難しく・複雑化 - アプリ開発者がより並行処理を扱いやすくするためのSwiftの設計思想 - まずは単一のメインスレッドで、順番どおりに動く仕組みを理解する。 - その上で async/await、そして並行処理へとステップアップしていくことを提案しています。

Slide 5

Slide 5 text

Default Actor Isolationとは Approachable Concurrencyを実現するための一つの機能で、デフォルトで安全な スレッド実行を保証する仕組み。 Default Actor IsolationをMainActorに指定することで、コードの多くが自動的に MainActor上で実行。(@MainActor を明示しなくてもUI操作は安全に) ※Default Actor Isolationは、Main Threadのほかnonisolated に設定することもできる。この場合、クラス・関数等はデフォ ルトでどのActorにも隔離されない状態になる

Slide 6

Slide 6 text

今回使用するアプリ - リスト表示とコレクションビューがタ ブ切り替え可能 - コレクションビューでは、Assetsから取 得したpng画像をサムネイル用にリサイ ズする処理が並列で走る - (Apple Instruments Tutorialsに登場するアプ リを自前でSwift6対応したもの) - 環境 - Xcode 26.0.1 - iOS 26.0.1

Slide 7

Slide 7 text

Approachable Concurrencyを有効にする

Slide 8

Slide 8 text

アプリの挙動 サムネイル画像のロードがもっさりし、ス クロール操作もカクつきがある・・・

Slide 9

Slide 9 text

Instruments(1/2) リスト表示→コレクションビューへの切り 替えのタイミングでhangしている CPU負荷が80〜100%でMain Threadが 高負荷な状態

Slide 10

Slide 10 text

Instruments(2/2) 高解像度の画像をサムネイル表示用にリサイ ズしている処理がMainスレッドで実行されて いそう

Slide 11

Slide 11 text

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で実行する)

Slide 12

Slide 12 text

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) メインスレッドス レッドで実行され るように・・

Slide 13

Slide 13 text

// 画像のリサイズ nonisolated private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat, displayScale: CGFloat) async -> UIImage? { … } コードを変更してみる nonisolatedを明示しても、 メインスレッドで実行され てしまった・・

Slide 14

Slide 14 text

再度Xcodeの設定を見てみる - Approachable Concurrencyの設定と連動して、Upcoming Features内の nonisolated(nonsending) By Default という設定が有効になっていた。

Slide 15

Slide 15 text

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)

Slide 16

Slide 16 text

以前のnonisolated asyncを再現するには? - Swift6.2で追加された @concurrent を付与する - Swift6.1以前のnonisolated asyncの挙動と同様、呼び出し元Actorとは独立し て並行実行される - @concurrent 属性を付与した場合、その関数は nonisolated の扱いになる。 - 下記例のように、Actor隔離された状態にアクセスできない

Slide 17

Slide 17 text

@concurrent private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat, displayScale: CGFloat) async -> UIImage? { … } 再度コードを修正...

Slide 18

Slide 18 text

アプリの挙動 リスト→コレクションビューへの切替え でもUIのカクつきがなくなった

Slide 19

Slide 19 text

Instruments hangを解消できた 画像のリサイズ処理もバックグラウン ドで実行されるように!

Slide 20

Slide 20 text

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