Swiftでの非Sendable型の扱い方
問題概要
NSExtensionContext
などの非Sendable型をAsyncコンテキストで使用する際、Swiftコンパイラから次のような警告が発生します:
Main actor-isolated property 'url' can not be mutated from a Sendable closure; this is an error in Swift 6
この問題の本質は:
- アクター越境の問題:
NSExtensionContext
が非Sendableのため、異なるアクター間での安全な共有が保証されない - UIViewController: デフォルトで
@MainActor
に隔離されているため、メインスレッド外からのプロパティ変更が制限される - Swift 6互換性: 現在の警告はSwift 6でコンパイルエラーとなる可能性が高い
推奨ソリューション
1. クロージャベースアプローチの修正
swift
attachment.loadItem(forTypeIdentifier: "public.url") { [weak self] url, error in
guard let url = url as? URL else { return }
// メインアクターでプロパティ更新
Task { @MainActor in
self?.url = url
}
}
なぜこの解決策か
loadItem
のコールバックはバックグラウンドスレッドで実行される可能性があるTask { @MainActor in }
でメインアクター上の安全な更新を保証- オリジナル実装の潜在的な競合状態を解消
2. async/awaitアプローチの最適化
swift
private func fetchURL() async {
guard let extensionContext = self.extensionContext,
let item = extensionContext.inputItems.first as? NSExtensionItem,
let attachments = item.attachments else { return }
for attachment in attachments {
do {
// 型を明示的に指定してキャスト
let result = try await attachment.loadItem(forTypeIdentifier: "public.url")
if let url = result as? URL {
Task { @MainActor in
self.url = url
}
}
} catch {
print("Error loading item: \(error)")
}
}
}
注意点
Foundationフレームワークは完全なSendable対応を完了していないため、@preconcurrency
アノテーションを追加します:
swift
@preconcurrency import UIKit
3. 安全なDetached Taskの使用
swift
private func fetchURL() async {
guard let attachments = await self.fetchAttachments() else { return }
Task.detached {
await self.processAttachments(attachments)
}
}
private func fetchAttachments() async -> [NSItemProvider]? {
return await MainActor.run { [weak self] in
guard let extensionContext = self?.extensionContext,
let item = extensionContext.inputItems.first as? NSExtensionItem else {
return nil
}
return item.attachments
}
}
private func processAttachments(_ attachments: [NSItemProvider]) async {
for attachment in attachments {
guard let url = try? await attachment.loadItem(forTypeIdentifier: "public.url") as? URL else {
continue
}
await MainActor.run { [weak self] in
self?.url = url
}
}
}
非推奨のアプローチ
@unchecked Sendableの回避
swift
// 非推奨:安全保証がない
extension NSExtensionContext: @unchecked Sendable {}
危険性
- コンパイラのデータ競合チェックを無効化する
- 実際のスレッド安全性を手動で検証する必要がある
- 将来のランタイムクラッシュのリスクが高い
理由のないDetached Taskの回避
swift
// 非推奨:不要なアクター境界越境
Task.detached { [weak self] in
guard let extensionContext = await self?.extensionContext else { return }
// ...
}
根本原因とSwiftの設計
Sendableプロトコルの役割
- 型安全性チェック: アクター間で安全に共有可能な型をマーク
- データ競合防止: コンパイル時の分析で並行処理バグを検出
- Swift 6対応: 新厳格モードでのエラー防止
NSExtensionContextの問題点
- Objective-C由来のクラスで
Sendable
適合が不足 - 状態管理が不透明でスレッド安全性が保証されない
- Appleフレームワークの完全対応が進行中
ベストプラクティス
最小限のメインアクター利用:
swift// Good Task { @MainActor in /* UI更新のみ */ } // Bad Task { @MainActor in /* 重い処理 */ }
非同期コンテキストでのプロパティアクセス:
swiftextension UIViewController { nonisolated var safeExtensionContext: NSExtensionContext? { MainActor.run { [weak self] in self?.extensionContext } } }
状態の局所化原則:
swiftprivate func processAttachment(_ attachment: NSItemProvider) async -> URL? { return try? await attachment.loadItem(forTypeIdentifier: "public.url") as? URL }
将来の進化対応
- SE-0414 (Region based isolation): 将来的な警告の自動解消を期待
- AppleフレームワークのSendable対応: 公式適合が完了すれば
@preconcurrency
不要に - Objective-C APIの置換:
NSExtensionContext
のSwiftネイティブ代替の登場可能性
結論
非Sendable型との適切な対処法:
- アクター境界の明確化:
@MainActor
でUI更新を隔離 - 危険な回避策の不使用:
@unchecked Sendable
は最終手段 - プロパティアクセスの分離: 非同期処理とUI更新を分離
- Foundationの暫定対策:
@preconcurrency import
で警告抑制
このアプローチにより、Swift 6の厳格な並行性チェックに準拠した安全なコードを維持できます。