Skip to content

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プロトコルの役割

  1. 型安全性チェック: アクター間で安全に共有可能な型をマーク
  2. データ競合防止: コンパイル時の分析で並行処理バグを検出
  3. Swift 6対応: 新厳格モードでのエラー防止

NSExtensionContextの問題点

  • Objective-C由来のクラスでSendable適合が不足
  • 状態管理が不透明でスレッド安全性が保証されない
  • Appleフレームワークの完全対応が進行中

ベストプラクティス

  1. 最小限のメインアクター利用:

    swift
    // Good
    Task { @MainActor in /* UI更新のみ */ }
    
    // Bad
    Task { @MainActor in /* 重い処理 */ }
  2. 非同期コンテキストでのプロパティアクセス:

    swift
    extension UIViewController {
        nonisolated var safeExtensionContext: NSExtensionContext? {
            MainActor.run { [weak self] in
                self?.extensionContext
            }
        }
    }
  3. 状態の局所化原則:

    swift
    private 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型との適切な対処法:

  1. アクター境界の明確化: @MainActorでUI更新を隔離
  2. 危険な回避策の不使用: @unchecked Sendableは最終手段
  3. プロパティアクセスの分離: 非同期処理とUI更新を分離
  4. Foundationの暫定対策: @preconcurrency importで警告抑制

このアプローチにより、Swift 6の厳格な並行性チェックに準拠した安全なコードを維持できます。