Swift 6.2でUIがモッサリするようになった話
先日開催された『Sansan x ヤプリ x ディップ 3社合同モバイル勉強会』で登壇した際の内容を記事にしました。
概要
Xcode 26,Swift 6.2 にアップデート後、個人開発のアプリで意図せず負荷の高い操作がMainスレッドで実行されるようになりました。
言語仕様の更新内容を調査していった結果、挙動変更の背景を理解できてきたので整理しておきます。
Approachable Concurrencyとは
Swift6でStrict Concurrency Checkingが導入され、データ競合をコンパイル時に検出できるようになりました。その一方でSendableやActorの扱いが複雑化したため、アプリ開発者がより並行処理を扱いやすくするためのSwiftの設計思想です。
まずは単一のメインスレッドで、順番どおりに動く仕組みを理解し、その上でasync/await、そして並行処理へとステップアップしていくことが提案されています。
Default Actor Isolation
Approachable Concurrencyを実現するための一つの機能で、デフォルトで安全なスレッド実行を保証する仕組みです。
- Default Actor IsolationをMainActorに指定することで、コードの多くが自動的にMainActor上で実行されます。
- (※Default Actor IsolationはMain Threadのほかnonisolatedに設定することもできます。)
動作確認環境
- Xcode 26.0.1
- iOS 26.0.1
今回使用するアプリ
- リスト表示とコレクションビューがタブ切り替え可能です。
- コレクションビューでは、複数枚のAssetsから取得したpng画像をサムネイル用にリサイズする処理が並列で実行されます。
- こちらはInstruments Tutorialsに登場するアプリを自前でSwift6対応したものです。(個人開発中のアプリとは異なります)

Xcodeの設定
Swift Compiler - Concurrencyの設定で、Approachable Concurrencyを Yes、Default Actor IsolationをMainActorにしています。

発生した現象
サムネイル画像のロードがもっさりし、スクロール操作もカクつきが出るようになりました。

Instrumentsによる解析
- リスト表示からコレクションビューへの切り替えのタイミングでhangしていることが確認できました。

- Swift Tasksプロファイルを見てみると、多数のTaskが立ち上がっていますが、それぞれのTaskがMainActor上で順番に実行されており、並列で実行できていません。
-
ImageFile.resizeImage(for:maxThumbnailSize:displayScale:)という高解像度の画像をサムネイル表示用にリサイズしている処理がMain Thread上で実行されており、高負荷な状態です。

暗黙的なMainActor実行と仕様変更
ビューのコード(ThumbnailView)
まずは、サムネイル画像のビューのコードです。
body プロパティ内の taskモデファイヤの箇所が、先程のSwift Tasksで表示されていたTaskに該当します。
taskのクロージャ内で、はMainActorで実行されます。
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 {
…
該当のコード(ImageFile)
画像のリサイズ処理を含むImageFileのコードは以下の通りです。
struct ImageFile: Identifiable {
// ...
// サムネイル画像の生成
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? {
// ... リサイズ計算
return image.preparingThumbnail(of: newSize)
}
}
ImageFileはどのActorにも隔離されていないnonisolatedなstructです。
しかし、Default Actor IsolationがMainActorに設定されているため、async 関数はMainActorで実行されるように振る舞い、リサイズ処理(resizeImage)もメインスレッドで実行されることになっていました。
nonisolatedを付与
resizeImage 関数に nonisolated を明示的に追加してみましたが、これも同様にメインスレッドで実行されてしまいました。
// 画像のリサイズ
nonisolated private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat, displayScale: CGFloat) async -> UIImage? {
// ...
}
Swift 6.2でのnonisolated asyncの振る舞いの変更について
これは、Swift 6.2で追加された設定 nonisolated(nonsending) By Default が Approachable Concurrencyと連動して有効になっていたことに起因していました。

Swift 6.1以前のnonisolated asyncは「呼び出し元のActor外での実行」(※1)でしたが、Swift 6.2ではnonisolated(nonsending) By Defaultの設定に依存し、呼び出し元(今回は MainActor)で実行されるように変更されたため、意図せずHangが発生しました。
nonisolated
func synchronousNonIsolated() {
...
}
nonisolated
func asynchronousNonIsolated(data: SendableType) async {
...
}
// Swift 6.2で追加
nonisolated(nonsending)
func asynchronousNonIsolatedNonsending(data: NonSendableType) async {
...
}
| 属性 | Swift6.1以前 | Swift6.2 | 補足 |
|---|---|---|---|
nonisolated |
呼び出し元のActorで実行される | (変更なし) | |
nonisolated async |
呼び出し元のActor外で実行される | nonisolated(nosending) By Default の設定が有効時は、nonisolated(nonsending) asyncと同じ挙動になる | |
nonisolated(nonsending) async(※2) |
- | 呼び出し元のActorで実行される | nonisolated(nosending) By Default の設定が有効時は、nonisolated(nonsending) asyncと同じ挙動になる |
(※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.)
@concurrentの導入
Swift 6.1以前のnonisolated asyncの挙動を再現するには、Swift 6.2で追加された @concurrent 属性を付与します。
- 注)
@concurrent属性を付与した場合、その関数はnonisolatedの扱いになるので、下記例のように、Actor隔離された状態にアクセスできない

修正後のコード
画像のリサイズ処理を行う resizeImage 関数に @concurrent を付与しました。
struct ImageFile: Identifiable {
// ...
// 画像のリサイズ
@concurrent // ✨これを付与して並行実行を保証
private func resizeImage(_ image: UIImage, maxThumbnailSize: CGFloat, displayScale: CGFloat) async -> UIImage? {
// ... リサイズ処理
return image.preparingThumbnail(of: newSize)
}
}
結果
リストからコレクションビューへの切替えでもUIのカクつきがなくなりました。

Instrumentsで確認すると、hangを解消でき、画像のリサイズ処理もバックグラウンドで実行されるようになったことが確認できました。

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