ファイル共有の実装方法と落とし穴

-- Views

October 29, 26

スライド概要

pixiv App Talk (2026年10月29日)での発表資料。iOSにおけるデータ共有の実装方法とtipsを紹介する。

profile-image

Rustacean。Vimmer。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

ファイル共有の実装方法と落とし穴 pixiv Inc. Tengu712

2.

データ共有の実装方法と tips pixiv Inc. Tengu712

3.

自己紹介 • Tengu712 • 2026 年 4 月新卒入社 • お絵描きツール Pastela の開発に従事 • 少々 iOS • 少々 Web フロント • 最近インフラ • Rustacean Vimmer

4.

データ共有 • アプリ間のデータの受渡し • Share • Drag & Drop • Open With (iOS 26+) • AirDrop • etc… • 意外と需要がないので文献が少ない

5.

データ共有 • アプリ間のデータの受渡し • Share • Drag & Drop • Open With (iOS 26+) • AirDrop • etc… • 意外と需要がないので文献が少ない

6.

ユースケース • URL を受け取ってリストに追加 • PDF を受け取って解析・記録 • 動画を受け取って加工・投稿 • etc…

7.

話すこと • 実装方法 • 単に API を叩くだけではなく、案外考慮すべきことがある

8.

実装 (共有元) • ファイル URL を Share Sheet に渡す • SwiftUI: ShareLink view • UIKit: UIActivityViewController let fileURL = URL.documentsDirectory.appending( path: "FILE_PATH", directoryHint: .notDirectory ) // SwiftUI ShareLink(item: fileURL) { Label("share", systemImage: "...") } // UIKit let sheet = UIActivityViewController( activityItems: [fileURL], applicationActivities: nil ) present(sheet, animated: true)

9.

実装 (共有先) • Info.plist に追加 • Drag & Drop が可能に • Share Sheet にアプリアイコンが出現 <key>CFBundleDocumentTypes</key> <array> <dict> <key>CFBundleTypeName</key> <string>任意の名前</string> <key>LSHandlerRank</key> <string>形式に対するアプリの立場</string> <key>CFBundleTypeRole</key> <string>形式に対するアプリの役割</string> <key>LSItemContentTypes</key> <array> <string>形式</string> ... </array> </dict> ... </array>

10.

実装例 (共有先) <key>CFBundleDocumentTypes</key> <array> <dict> <key>CFBundleTypeName</key> <string>Image File</string> <key>LSHandlerRank</key> <string>Default</string> <key>CFBundleTypeRole</key> <string>Viewer</string> <key>LSItemContentTypes</key> <array> <string>public.png</string> <string>public.jpeg</string> </array> </dict> <!-- com.example.myfileは UTExportedTypeDeclarationsで 定義されているものとする --> <dict> <key>CFBundleTypeName</key> <string>My File</string> <key>LSHandlerRank</key> <string>Owner</string> <key>CFBundleTypeRole</key> <string>Editor</string> <key>LSItemContentTypes</key> <array> <string>com.example.myfile</string> </array> </dict> </array>

11.
[beta]
実装 (共有先)
// SwiftUI
@main struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
// ...
}
}
}
}

// UIKit
class SceneDelegate: UIWindowSceneDelegate {
func scene( // 起動時
_ scene: UIScene,
willConnectTo session: UISceneSession,
options options: UIScene.ConnectionOptions
) {
let urls = options.urlContexts.map(\.url)
// ...
}
func scene( // 復帰時
_ scene: UIScene,
openURLContexts contexts: Set<UIOpenURLContext>
) {
let urls = contexts.map(\.url)
// ...
}
}

12.

実装例 (共有先) @main struct MyApp: App { @State private var image: UIImage? var body: some Scene { WindowGroup { Group { if let image { Image(uiImage: image) .resizable() .scaledToFit() } else { EmptyView() } } .onOpenURL { url in image = UIImage(contentsOfFile: url.path) } } } }

14.

Open in Place • 共有元のファイルを直接参照する設定 • 共有元が in place に共有している • かつ共有先アプリが対応している • Files の Open With が利用可能に // 共有元 let provider = NSItemProvider() provider.registerFileRepresentation( for: .png, visibility: .all, openInPlace: true // ← これ ) { completion in completion(url, true, nil) return nil } // 共有先 // project.pbxproj // Xcode上ではSupports Documents in Place INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;

16.

Security Scope • 権限問題 let isSecurityScoped = url.startAccessingSecurityScopedResource() • シミュレータでは問題なく動作 • 実機では権限エラー • Security scope 付きで開く必要 defer { if isSecurityScoped { url.stopAccessingSecurityScopedResource() } }

17.

スクリーンショット • スクリーンショットの共有は public.image である UIImage の keyed archive • 非ファイル • CFBundleDocumentTypes はファイル専用 • public.image を LSItemContentTypes に追加しても受け入れられない

18.

Share Extension • データ共有のための機能 • 共有時に view を表示できる • 共有元アプリを維持しつつ共有できる • 共有先アプリの自動起動は非推奨

19.

実装 1. Share Extension ターゲットを追加 2. NSExtensionActivationRule を設定 3. App Groups を設定 4. ShareViewController で共有データ保存 5. アプリから読み出して利用

20.

実装 1. Share Extension ターゲットを追加 2. NSExtensionActivationRule を設定 3. App Groups を設定 4. ShareViewController で共有データ保存 5. アプリから読み出して利用

21.

実装 1. Share Extension ターゲットを追加 2. NSExtensionActivationRule を設定 3. App Groups を設定 4. ShareViewController で共有データ保存 5. アプリから読み出して利用 <key>NSExtensionAttributes</key> <dict> <key>NSExtensionActivationRule</key> <dict> <!-- 画像データを最大5件受け入れる --> <key> NSExtensionActivationSupportsImageWithMaxCount </key> <integer>5</integer> <!-- 文字列データを受け入れる --> <key> NSExtensionActivationSupportsText </key> <true/> </dict> </dict>

22.

実装 1. Share Extension ターゲットを追加 2. NSExtensionActivationRule を設定 3. App Groups を設定 4. ShareViewController で共有データ保存 5. アプリから読み出して利用

23.
[beta]
実装
1. Share Extension ターゲットを追加
2. NSExtensionActivationRule を設定
3. App Groups を設定
4. ShareViewController で共有データ保存
5. アプリから読み出して利用

// extensionContextからNSItemProviderを取得
let provider = extensionContext.inputItems
.compactMap { $0 as? NSExtensionItem }
.flatMap { $0.attachments ?? [] }
.first!
// NSItemProviderからデータ・情報取得
let data =
try await provider.loadDataRepresentationAsync(
for: .image
)
// App Groupに保存
let destination = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "..."
)?
.appending(path: "...")
try data.write(to: destination!, options: .atomic)

26.

SUBQUERY • NSExtensionActivationSupports*を列挙 する方式では • 無制限の受入れ • 特定拡張子の拒否 • 独自ファイル形式の受入れができな い • SUBQUERY を書く必要 <key>NSExtensionActivationRule</key> <string> SUBQUERY( extensionItems, $item, SUBQUERY( $item.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.example.myfile" ).@count == $item.attachments.@count ).@count == 1 </string>

27.

loadItem • 唯一 NSItemProvider で async/await に 対応 • iOS 27 から deprecated • loadFileRepresentation や loadDataRepresentation で代替 func loadItem( forTypeIdentifier typeIdentifier: String, options: [AnyHashable : Any]? = nil ) async throws -> any NSSecureCoding

28.
[beta]
loadDataRepresentation
extension NSItemProvider {
func loadDataRepresentationAsync(for type: UTType) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
_ = loadDataRepresentation(for: type) { data, error in
if let error {
continuation.resume(throwing: error)
} else if let data {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: CocoaError(.fileReadUnknown))
}
}
}
}
}

29.

Isolation Boundary • 複数 NSItemProvider を並行処理したい場合 Task Groups を使いたい • NSItemProvider は非 Sendable → isolation boundary を越えられない • Swift 6 からはコンパイルエラー • 解決策: • Main Actor で Sendable まで処理する • Main Actor で並行化する • @unchecked Sendable を使う

30.

実装例 // MainActorで並行処理 await withTaskGroup(of: Void.self) { group in for provider in providers { group.addTask { @MainActor in let data = try? await provider.loadDataRepresentation(for: .image) // ... } } } // NSItemProviderをSendable化 extension NSItemProvider: @retroactive @unchecked Sendable { // NSItemProviderをwrap struct UncheckedSendableNSItemProvider: @unchecked Sendable { let value: NSItemProvider

31.

まとめ やりたいこと 共有元 共有先 ファイル共有 Share Sheet にファイル URL を渡す CFBundleDocumentTypes ファイル直接参照 openInPlace: true Supports Documents in Place: YES データ共有 Share Sheet にデータを渡す Share Extension • 共有元 sandbox 内のファイル URL は startAccessingSecurityScopedResource で security scoped URL 化 • CFBundleDocumentTypes はファイル専用なので、データにも対応するには Share Extension を用意する • Share Extension の受け入れをカスタマイズするには SUBQUERY を書く • loadItem は iOS 27 から deprecated • NSItemProvider は非 Sendable