端末ごとのアプリ内設定に追加したiCloud同期機能をオン/オフ操作したタイミングで、ローカルとiCloud Drive、それぞれにあるDocumentsフォルダ内のファイルを移動させたくてコードを書いたので、備忘として記録します。
Swift勉強中なので、もっとこうしたら良いよ!ここ間違ってるよ!などあれば教えていただけると嬉しいです。
開発環境
Xcode 12.0
ローカル→iCloud DriveのDocumentsにファイルを移動する
移動後に、ローカルのファイルを削除して良い場合は、FileManagerのsetUbiquitousを使います。
ローカルのファイルを残しておきたい場合は、FileManagerのcopyItemを使います。
Apple Developer Documentation – setUbiquitous
Apple Developer Documentation – copyItem
Apple Developer Documentationに、移動元と先でDirectory指定ができると書いてあったので、Directory丸ごとsetUbiquitousまたはcopyItemしようと思っていたのですが、思うようにいかなかったのでファイルを一つひとつ、移動元先のフルパスで指定しています。
// Documents Directory URL
let fm = FileManager.default
let localUrl = fm.urls(for: .documentDirectory, in: .userDomainMask)[0] // Local Document Directory
let iCloudUrl = fm.url(forUbiquityContainerIdentifier: iCloudContainerId)!.appendingPathComponent("Documents") // iCloud Document Directory
// ローカル->iCloud ファイル移動
func moveFilesToiCloud() {
DispatchQueue.main.async {
let fromUrl = localUrl
let toUrl = iCloudUrl
let enumerator = fm.enumerator(atPath: fromUrl.path)
while let file = enumerator?.nextObject() as? String {
do {
let fromFileUrl = fromUrl.appendingPathComponent(file)
let toFileUrl = toUrl.appendingPathComponent(file)
//try fm.copyItem(atPath: fromFileUrl.path, toPath: toFileUrl.path) // ローカルのデータ残したままiCloudにアップロードする
try fm.setUbiquitous(true, itemAt: fromFileUrl, destinationURL: toFileUrl) // iCloudへ転送し、ローカルのデータは削除する
print("ローカル->iCloud:ファイル転送に成功しました")
} catch {
print("ローカル->iCloud:ファイル転送に失敗しました <File: \(file)> <Error: \(error.localizedDescription)>")
}
}
}
}
iCloud Drive→ローカルのDocumentsにファイルを移動する
iCloudサーバ上にファイルの実体があり、ローカルのiCloud Driveにはまだダウンロードされていない状態の場合、本来のファイル名の先頭に”.”、末尾に”.icloud”という文字が付与されていて、単純にcopyItemではローカルのDocumentsフォルダにコピーできません。
調べたところ、iCloudサーバからローカルの特定フォルダへファイルを移動する場合は、FileManagerのsetUbiquitousを使うのが正解らしいです。実際、まだローカルのiCloud Drive上にダウンロードされていないファイルでも、簡単に移動できました。
Apple Developer Documentation – setUbiquitous
一つ問題があって、setUbiquitousが、ファイルを移動した後iCloud上のファイルを自動的に削除してしまう仕様なので、移動後もiCloud上にファイルを残しておきたい場合は、移動処理が完了した後に、ローカルからiCloudへcopyItemして戻す処理が必要になります。
私の場合、iCloud同期のスイッチをオンにした時にiCloudからローカルにファイルコピーしようとしているので、iCloud Driveのデータが消えてしまうと、iCloud同期をONにしている他端末からもデータが消えてしまうことになり、対応が必要でした。
iCloudからローカルにダウンロード、その後ローカルからiCloudに再アップロード、なので無駄なトラフィックが発生するし、すごく効率の悪いことをしている感じで嫌だったので、
他の方法として、iCloudからダウンロード前のファイルを、ファイル名末尾の”.icloud”を条件に検索してローカルへstartDownloadingUbiquitousItemでダウンロードしてからcopyItemする力技も試しましたが、
私のアプリでは想定しているファイルサイズが小さかったのと、頻繁に実行される処理ではないこと、そしてiCloud専用処理を使ったコードのシンプルさと動作の信頼感の高さで、今回はsetUbiquitousを使うことにしました。
以下がコードです。引数のremoveは、ユーザーにiCloud上のデータを残すか確認してtrue/falseを設定できるようにしています。
// Documents Directory URL
let fm = FileManager.default
let localUrl = fm.urls(for: .documentDirectory, in: .userDomainMask)[0] // Local Document Directory
let iCloudUrl = fm.url(forUbiquityContainerIdentifier: iCloudContainerId)!.appendingPathComponent("Documents") // iCloud Document Directory
// iCloud->ローカル ファイル移動
// iCloudのデータを削除するか、残すかはユーザーが選択する (remove=falseの場合、残す)
func moveFilesToLocal(remove: Bool) {
DispatchQueue.main.async {
let fromUrl = iCloudUrl
let toUrl = localUrl
let enumerator = fm.enumerator(atPath: fromUrl.path)
while let file = enumerator?.nextObject() as? String {
do {
let fromFileUrl = fromUrl.appendingPathComponent(file)
let toFileUrl = toUrl.appendingPathComponent(file)
try fm.setUbiquitous(false, itemAt: fromFileUrl, destinationURL: toFileUrl) // ローカルへ転送し、iCloudを削除
print("iCloud->ローカル:ファイル移行に成功しました")
} catch {
print("iCloud->ローカル:ファイル移行に失敗しました <File: \(file)> <Error: \(error.localizedDescription)>")
}
}
do {
if remove == false {
try fm.copyItem(atPath: toUrl.path, toPath: fromUrl.path) // setUbiquitousにより削除されたiCloudのデータを、ローカルのデータで元に戻す
}
} catch {
print("iCloud->ローカル:ファイル移行に失敗しました <Error: \(error.localizedDescription)>")
}
}
}
アプリの宣伝
上記のiCloud同期を実装したアプリがこちらです!よかったら使ってみてください☺️
コメント