はじめに
RealmファイルをiOSからwatchOSにfileTransferで送信しようとしたときに、実機ランでmake_dir() failed: Operation not permitted
というエラーが生じました。この解決方法について備忘のためにメモを残します。
現象
実機ランすると、iOSがwatchOSにRealmファイルをfileTransferしたときに、default.realm.management': make_dir() failed: Operation not permitted
というエラーが生じました。シミュレータでランしたときには、このエラーが発生しません。
Realm Swift 2.4.4、Xcode 8.2、iOS10.2.1、watchOS 3.1.1、シミュレータ(iOS/Watch)Version 10.0(SimulatorApp-236.7)という環境でした。
原因
watchOSがRealmファイルを受け取った後、そのファイルは読取り専用領域に保持されます。一方、Realmは管理のためにその領域に書き込みをしようとします。ここで権限がない操作が行われようとしたため、Operation not permitted
というエラーが発生していました。
このエラーがAppleWatch実機のみで発生し、一方でシミュレータで発生しないのは、シミュレータが書込み権限の動作を完全にシミュレートできていないためだと考えられます。
プログラマが意図してwatchOS側のRealmファイルを変更しようとしなくても、Realm自体が書込みをしようとするので、この挙動は意識しておく必要があります。
解決
エラーが出てしまう従来のコードは、こちらです。読み取り専用領域にファイルが作られるので、Realmが管理用に編集しようとしたときにエラーが発生します。
// When the file was received
func session(_ session: WCSession, didReceive file: WCSessionFile) {
//set the recieved file to default Realm file
var config = Realm.Configuration()
config.fileURL = file.fileURL
Realm.Configuration.defaultConfiguration = config
// display the first of realm objects
let realm = try! Realm()
if let firstField = realm.objects(Field.self).first{
realmLabel.setText(firstField.text)
}
}
解決のために、受信したファイルをそのまま利用するのではなく、そのコピーを書込み権限のあるdocumentDirectory配下に作り、そのパスを使うことにします。
加えて、同名のファイルがあるとFileManagerの重複ファイルエラーが発生してしまうので、その対応が必要になります。今回は、既存ファイルがあればそれを削除した後で、コピーするように実装します。
具体的には次のようなコードになります。
// When the file was received
func session(_ session: WCSession, didReceive file: WCSessionFile) {
//set the recieved file to default Realm file
var config = Realm.Configuration()
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
let realmURL = documentsDirectory.appendingPathComponent("data.realm")
if FileManager.default.fileExists(atPath: realmURL.path){
try! FileManager.default.removeItem(at: realmURL)
}
try! FileManager.default.copyItem(at: file.fileURL, to: realmURL)
config.fileURL = realmURL
Realm.Configuration.defaultConfiguration = config
// display the first of realm objects
let realm = try! Realm()
if let firstField = realm.objects(Field.self).first{
realmLabel.setText(firstField.text)
}
}
これで解決できました。
なお、FileManager.default.fileExists(atPath:)の引数には、realmURL.pathを代入します。引数がString型ですが、.absoluteStringや.relativeStringではありません。
参考リンク
今回のソースファイルは全部こちらのリポジトリで公開しています。
SampleRealmOnWatchOS3
基本的にこちらのコードの考え方を参考にしています。ただし、既存ファイルがあると重複エラーが発生するので、今回はその対応を加えて実装しました。
Apple WatchにRealmのデータを転送できない