LoginSignup
12
9

More than 1 year has passed since last update.

SwiftUI 2.0: iOSアプリにファイルをインポートする方法

Posted at

課題

仕事でSwiftUI 2.0を使用して構築したiOS14.0以降のアプリにaudioファイルをインポートしたかったが、security-scoped URLsとスタイリングで問題が発生したことで、私の経験を共有したいと思った...

ファイルピッカー

iOS 14ベータ6のリリースに伴い、Appleは、ユーザーがiOSデバイスまたはiCloudドライブから既存のファイルをインポートできるようにする新しいビュー修飾子を提供した。まず、この新しいfileImporter()修飾子を確認しましょう...

fileImporter修飾子

fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)メソッドを使用すると、ユーザーは1つ以上のファイルをインポートできる。

宣言
SwiftUI
func fileImporter(
    isPresented: Binding<Bool>, 
    allowedContentTypes: [UTType], 
    allowsMultipleSelection: Bool, 
    onCompletion: @escaping (Result<[URL], Error>) -> Void
) -> some View
パラメーター
  • isPresented: ファイルピッカーインターフェイスを表示するかどうかを支持するバインディングです。
  • allowedContentTypes: インポート可能なサポートされているコンテンツタイプのリストです。
  • allowsMultipleSelection: ユーザーが複数のファイルを同時に選択してインポートできるかどうかを支持するブール値です。
  • onCompletion: 操作が成功したときに呼び出されるコールバックです。
詳細
  • ファイルピッカーを表示するには、 isPresentedでバインドされている変数をtrueに設定する。
  • インポートするファイルが選択されると、 onCompletionコールバックが呼び出される前にisPresentedが自動的にfalseに戻される。
  • ユーザーがインポートをキャンセルすると、 isPresentedは自動的にfalseに戻され、onCompletionコールバックは呼び出されない。
  • onCompletionコールバックが呼び出されると、result.successまたは.failureのいずれかになる。
  • .successの場合、 resultには、インポートするファイルのURLsのリストが含まれる。
注意

allowedContentTypesは、ファイルインポータが表示されると同時に変更できru
が、すぐには効果がなく、次にファイルインポータが表示されたときにのみ適用される。

サンプルコード

1つのオーディオファイルをインポートして再生することで、実装のコードをテストしてみましょう:

SwiftUI
import SwiftUI
import AVFoundation
...
    @State private var isImporting = false
    var body: some View {
        Button(action: {
            isImporting = true
        }) {
        Image(systemName: "waveform.circle.fill")
            .font(.system(size: 40))
        }
        .fileImporter(
            isPresented: $isImporting,
            allowedContentTypes: [.audio],
            allowsMultipleSelection: false
        ) { result in
            if case .success = result {
                do {
                    let audioURL: URL = try result.get().first!

                    let audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
                    audioPlayer.delegate = ...
                    audioPlayer.prepareToPlay()
                    audioPlayer.play() // ← ERROR raised here

                } catch {
                    let nsError = error as NSError
                    fatalError("File Import Error \(nsError), \(nsError.userInfo)")
                }
            } else {
                print("File Import Failed")
            }
        }
    }
...

何が起こった?!

エラー

下記は、実際の実装のコードを実行したときにXCodeコンソールで発生したエラーです:

XCode
2021-04-23 10:00:01.123456+0900 MyApp[10691:1234567] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
2021-04-23 10:00:01.123456+0900 MyApp[10691:1234567] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
Playback initialization for "file:///private/var/mobile/Containers/Shared/AppGroup/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/File%20Provider%20Storage/Voice/TestFile.m4a" file has failed: Error Domain=NSOSStatusErrorDomain Code=-54 "(null)".
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file MyApp/AudioPlayer.swift, line 123

エラーの意味

iOS上のアプリはサンドボックス化されており、サンドボックス内のファイルへのアクセスは無制限ですが、適切なアクセス許可がない時、サンドボックス外のファイルへのアクセスは制限される。

アプリのサンドボックス外のファイルにアクセスするためのアクセス許可を取得するにはどうすればよいですか?

セキュリティ権限

インポートするファイルのURLのリストを取得したら、それらにアクセスするためのアクセス許可を取得する必要があります。

startAccessingSecurityScopedResource() メソッド

宣言
Swift
func startAccessingSecurityScopedResource() -> Bool
リターンバリュー

この関数は、ファイルへのアクセス要求が成功した場合はtrueを返し、それ以外の場合はfalseを返す。

詳細

セキュリティスコープのURLを取得した場合、それが指すファイルをすぐに使用することはできない。

ファイルにアクセスするには、セキュリティスコープのURLでstartAccessingSecurityScopedResource()メソッド(またはCore Foundationの同等のCFURLStartAccessingSecurityScopedResource(_:)関数)を呼び出す必要があり、これによりファイルの場所がアプリのサンドボックスに追加され、ファイルはアプリで利用可能になる。

ファイルへのアクセスを正常に取得した後、ファイルを使い終わったらすぐにファイルへのアクセスを放棄する必要があります。

そのために、URLでstopAccessingSecurityScopedResource()メソッド(またはそれに相当するCore Foundationの同等のCFURLStopAccessingSecurityScopedResource(_:)関数)を呼び出すとアクセスを放棄し、すぐにファイルへのアクセスができなくなる。

注意

ファイルシステムリソースが不要になったときにアクセスを放棄しなかった場合、アプリはカーネルリソースが漏れる。多くのカーネルリソースが漏れたら、アプリは再起動されるまでサンドボックスにファイルシステムの場所を追加できなくなる。

サンプルコード
SwiftUI
...
    @State var audioFiles: Array<MediaFileObject> = Array<MediaFileObject>()
    @State private var isImporting = false
    var body: some View {
        Button(action: {
            isImporting = true
        }) {
        Image(systemName: "waveform.circle.fill")
            .font(.system(size: 40))
        }
        .fileImporter(
            isPresented: $isImporting,
            allowedContentTypes: [.audio],
            allowsMultipleSelection: false
        ) { result in
            if case .success = result {
                do {
                    let audioURL: URL = try result.get().first!
                    if audioURL.startAccessingSecurityScopedResource() {
                        audioFiles.append(AudioObject(id: UUID().uuidString, url: audioURL))
                    }
                } catch {
                    let nsError = error as NSError
                    fatalError("File Import Error \(nsError), \(nsError.userInfo)")
                }
            } else {
                print("File Import Failed")
            }
        }
    }
...

ファイルタイプ

他のアプリから読み込む・保存する・開くリソースには、共有のシステムデータ形式を使用できる。または、必要に応じて独自のファイルとデータ形式を定義できる。

私のコードでは、すべての種類のオーディオファイルを表し、「画像、オーディオ、およびビデオの基本タイプ」カテゴリに属する​​.audioタイプを使用した;

  • image: 画像データを表す基本タイプ。
  • audio: ビデオを含まないオーディオを表すタイプ。
  • audiovisualContent: オーディオを含む場合と含まない場合があるビデオコンテンツを含むデータを表す基本タイプ。
  • movie: ビデオとオーディオの両方を含む可能性のあるメディア形式を表す基本タイプ。
  • video: 音声を含まないビデオを表すタイプ。

スタイリング

SwiftUI 2.0(SwiftUI 3.0を期待)では、スタイリングはまだ実際には簡単ではないが、ファイルピッカーの外観を変更するいくつかの方法を見つけた:

  • UINavigationBar.appearance().tintColor: UIColor: ファイルピッカーのヘッダーにあるツールバー項目の色です。
  • UITabBar.appearance().barTintColor: UIColor: ファイルピッカーの下部にあるタブバーの背景色です。
  • UITabBar.appearance().tintColor: UIColor: 現在選択されているタブバーアイテムの色です。
  • UITabBar.appearance().unselectedItemTintColor: UIColor: 現在選択されていないツールバーアイテムの色です。

注意してください ! UIKit設定をこのように使用すると、アプリケーションの他の部分のナビゲーションバーとタブバーの外観に影響を与える可能性がある。

最後に

この記事が、アプリにファイルのインポートを実装するのに役立つことを願っています。

SwiftUI 1.0のやり方?

別の方法で(fileImporter()なし)オーディオファイルをインポートするファイルピッカーを実装しようとしたができなかった!

fileExporter修飾子

fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:)メソッドを使用すると、ユーザーはメモリ内のドキュメントをディスク上のファイルにエクスポートできる。

fileMover修飾子

fileMover(isPresented:file:onCompletion:)メソッドを使用すると、ユーザーは既存のファイルを新しい場所に移動させることができる。

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9