課題
仕事でSwiftUI 2.0を使用して構築したiOS14.0以降のアプリに_audio_ファイルをインポートしたかったが、__security-scoped URL__sとスタイリングで問題が発生したことで、私の経験を共有したいと思った...
ファイルピッカー
iOS 14ベータ6のリリースに伴い、Appleは、ユーザーが__iOSデバイス__または__iCloudドライブ__から既存のファイルをインポートできるようにする新しいビュー修飾子を提供した。まず、この新しいfileImporter()
修飾子を確認しましょう...
fileImporter
修飾子
fileImporter(isPresented:allowedContentTypes:allowsMultipleSelection:onCompletion:)
メソッドを使用すると、ユーザーは1つ以上のファイルをインポートできる。
宣言
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
には、インポートするファイルの__URL__sのリストが含まれる。
注意
allowedContentTypes
は、ファイルインポータが表示されると同時に変更できru
が、すぐには効果がなく、次にファイルインポータが表示されたときにのみ適用される。
サンプルコード
1つのオーディオファイルをインポートして再生することで、実装のコードをテストしてみましょう:
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コンソールで発生したエラーです:
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()
メソッド
宣言
func startAccessingSecurityScopedResource() -> Bool
リターンバリュー
この関数は、ファイルへのアクセス要求が成功した場合は__true__を返し、それ以外の場合は__false__を返す。
詳細
セキュリティスコープのURLを取得した場合、それが指すファイルをすぐに使用することはできない。
ファイルにアクセスするには、セキュリティスコープのURLでstartAccessingSecurityScopedResource()
メソッド(またはCore Foundationの同等のCFURLStartAccessingSecurityScopedResource(_:)
関数)を呼び出す必要があり、これによりファイルの場所がアプリのサンドボックスに追加され、ファイルはアプリで利用可能になる。
ファイルへのアクセスを正常に取得した後、ファイルを使い終わったらすぐにファイルへのアクセスを放棄する必要があります。
そのために、URLでstopAccessingSecurityScopedResource()
メソッド(またはそれに相当するCore Foundationの同等のCFURLStopAccessingSecurityScopedResource(_:)
関数)を呼び出すとアクセスを放棄し、すぐにファイルへのアクセスができなくなる。
注意
ファイルシステムリソースが不要になったときにアクセスを放棄しなかった場合、アプリはカーネルリソースが漏れる。多くのカーネルリソースが漏れたら、アプリは再起動されるまでサンドボックスにファイルシステムの場所を追加できなくなる。
サンプルコード
...
@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:)
メソッドを使用すると、ユーザーは既存のファイルを新しい場所に移動させることができる。