iOSでデータを永続化する方法
作成日時: 2016-07-01
iOS version: 9
この投稿では、iOSのファイルシステムについて理解し、データを永続化(iCloud含む)する方法を紹介する。尚、サンプルコードは動かない可能性もあるので参考程度にして下さい。
iOS File System
アプリがファイルシステムとやり取り出来る場所は、ほぼアプリのサンドボックス内のディレクトリに制限されている。新しいアプリがインストールされる際、インストーラーはサンドボックス内に複数のコンテナを作成し、図1に示す構成をとる。各コンテナには役割があり、Bundle Containerはアプリのバンドルを保持し、Data Containerはアプリとユーザ両方のデータを保持する。Data Containerは用途毎に、さらに複数のディレクトリに分けられる。アプリは、例えばiCloud Containerのように、実行時に追加のコンテナへのアクセスをリクエストすることもある。
図1. An iOS app operating within its own sandbox
表1は、各コンテナ内の重要なサブディレクトリについて、その用途やアクセス制限についてまとめたものである。ファイルの種類とそれに対応する保存場所はガイドラインに明記してあり、それに反した場合はリジェクトの対象となるので注意が必要。
表1. Commonly used directories of an iOS app
ディレクトリ | 説明 | iTunes, iCloud バックアップ対象 |
---|---|---|
AppName .app |
メインバンドルと呼ばれている。アプリとアプリのリソースファイルを含んでいるディレクトリ。読み取り専用。 | NO |
Documents/ | ユーザが作成したコンテンツ等を保存しておく場所。 | YES |
Documents/Inbox/ | 他のアプリからファイルを受け取るときに使用するディレクトリ。読み取り専用。 | YES |
Library/ | アプリで利用するデータを保存しておく場所。よく使われるサブディレクトリにApplication SupportとCachesがあるが、カスタムディレクトリを作ることも可能。 | YES |
Library/Application Support/ | アプリで利用するデータを保存しておく場所。 | YES |
Library/Caches/ | 一時的なデータや、後から再度ダウンロードして復旧可能なデータを保存する場所。アプリ側でデータの追加、削除を管理する必要あり。ただし、ディスク・スペースが足りない等の理由でシステムが自動削除する可能性がある。 | NO |
Library/Preferences/ | NSUserDefaultsのデータが保存される場所。 | YES |
tmp/ | 一時的なデータを保存する場所。使い終わったらアプリ側で削除する必要あり。ただし、アプリが動作してないときにシステムが自動削除する可能性がある。 | NO |
Path to Directory
各種ディレクトリパスの取得方法
let manager = FileManager.default()
// Documents/
let documentDir = manager.urlsForDirectory(.documentDirectory, inDomains: .userDomainMask)[0]
// Library/
let libraryDir = manager.urlsForDirectory(.libraryDirectory, inDomains: .userDomainMask)[0]
// Library/Application Support/
let applicationSupportDir = manager.urlsForDirectory(.applicationSupportDirectory, inDomains: .userDomainMask)[0]
// Library/Caches/
let cachesDir = manager.urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask)[0]
// tmp/
let tmpDir = manager.temporaryDirectoy
Write/Read Files
iOSではオブジェクトをファイルに保存する際に
- アーカイブ(シリアライズ)
- プロパティリスト(*.plist)
を使う2通りがある。アーカイブは単純に、オブジェクトをNSData型に変換(これをアーカイブという)してからファイルに保存する方法。またNSArrayとNSDictionaryクラスは、それぞれプロパティリストとして保存する仕組みが提供されている。
アーカイブして保存する例
// UIImageの保存
let imagePath = documentDir.appendingPathComponent("sample.png")
let image = UIImage(named: "bundle_image.png")
// 書き込み
UIImagePNGRepresentation(image)?.write(to: imagePath, atomically: true)
// 読み込み
UIImage(data: NSData(contentsOf: imagePath))
// Stringの保存
let strPath = documentDir.appendingPathComponent("sample.txt")
let str = "Sample text"
let data = str.data(using: .utf8)
// 書き込み
data?.write(to: strPath)
// 読み込み
String(data: Data(contentsOf: strPath), encoding: .utf8)
プロパティリストとして保存する例
let path = documentDir.appendingPathComponent("sample.plist")
var user = NSDictionary(dictionary: ["Name":"Tim Cook"])
// 書き込み
user.write(to: path, atomically: true)
// 読み込み
var user = NSDictionary(contentsOf: path)
Data Managing Services
SDKにはデフォルトでデータを管理する仕組みが用意されていて、用途に応じてNSUserDefaultsやCore Dataを使って管理が可能である。なお、Core Dataの代わりにRealmというモバイル向けデータベースを使うケースも増えてきているが、ここでは割愛する。
表2. Commonly used services for managing data in iOS app
タイプ | 説明 |
---|---|
NSUserDefaults | Key/Value方式で任意のオブジェクトの保存・読み込みが可能。plistとして保存される。 |
Core Data | SQLite用のORM。DBファイルはDocuments/以下に作られる。 |
Keychain | 暗号化されたストレージ。パスワードや秘密鍵、証明書などを安全に保存する用途として使われる。 |
iCloud Storage
iCloud上にデータを保存しておくことで、複数端末から同じデータへのアクセスが可能となる。iCloudでは表3に示す3つのストレージタイプがあり、用途に応じて使い分ける。各サービスを利用するには、Xcode projectのCapabilitiesタブでiCloudをONにして、設定しておく必要がある。
表3. Types of storage service in iCloud
タイプ | 説明 |
---|---|
Key-value storage | アプリの設定や状態などを保存するためのKVS。最大容量は1アプリ1MB。 |
iCloud Documents | ファイルベースのストレージ(Core Data含む)。ドキュメントやドローファイル、または複雑なアプリの状態などを格納するために利用可能。最大保存容量はユーザーのiCloudアカウントに依存。 |
CloudKit storage | Appleが提供するBaaSの内のストレージ機能。構造化されたデータを管理したり、ユーザー同士で共有したいファイルやデータを保存したい際に利用する。データの管理にはデータベースが使用され、各レコードはKVSとして扱う。 |
Key-value storage
iCloud Key-value storageには、NSUbiquitousKeyValueStoreクラスを使ってアクセスする。書き込みするデータはまずメモリで保持され、その後システムによって適切なタイミングでディスクに書き込まれる。もしiCloudアカウントにサインインしていない状態で書き込みを行った場合、データは次の同期までローカルに保存されておく。ユーザがiCloudアカウントにサインインしたら、システムが自動的にローカルのKVSとiCloudサーバとの帳尻を合わせてくれる。
コード例
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let kvs = NSUbiquitousKeyValueStore.default()
// register to observe notifications from the store
NSNotificationCenter.defaultCenter()
.addObserver(self,
selector: #selector(storeDidChange:),
name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification,
object: kvs)
// get changes that might have happened while this
// instance of your app wasn't running
kvs.synchronize()
// Get/Set value
kvs.set("Value", forKey: "Key")
kvs.string(forKey: "Key")
return true
}
iCloud Documents
アプリで扱うドキュメント(UIDocumentを継承したクラスのオブジェクト)をiCloud上に保存しておくサービス。ここで言うドキュメントは、単一のファイルやファイルパッケージとしてディスクに書き込むことができるデータの集合体である。ローカルではiCloud Containerにファイルが置かれ、Documents/以下のファイルはユーザに公開される。それ以外に置かれているファイル、ディレクトリはユーザに非公開である。
ドキュメントはデフォルトでiCloud apiに対応しており、iCloud上のファイル変更とローカルでのファイル変更を上手く調整してくれる。これはNSFileCoordinatorクラスのオブジェクトとNSFilePresenterプロトコルによって実現されている。ファイルの開く、保存、名前変更をするためのUIはアプリ上で実装する必要がある。
コード例
let manager = FileManager.default()
let containerPath = manager.urlForUbiquityContainerIdentifier(nil)
let documentPath = containerPath.appendingPathComponent("Documents")
let filePath = documentPath.appendingPathComponent("document.key")
// Create document file to iCloud Container
let document = UIDocument(fileURL: filePath)
document.save(to: document.fileURL, for: .forCreating)
// Download is automatically done by the OS
またCore Dataも、UIManagedDocumentクラス(UIDocumentのサブクラス)を利用することで、同じ仕組でiCloudとの同期を取ることができる。
CloudKit
基本的には他のユーザと共有するデータを保存しておくサービス。各レコードはKVSとして扱われ、単純なタイプ(文字列、数字、日付など)から複雑なタイプ(位置情報、参照、アセットなど)のオブジェクトまで対応している。
他のiCloudサービスと同じで、データはローカルのコンテナで作成されシステムによって同期が取られる。CKContainerオブジェクトでコンテナ内にあるデータベースCKDatabaseオブジェクトにアクセスできる。
コード例
let container = CKContainer.default()
let database = container.publicCloudDatabase
// Create record
let record = CKRecord(recordType: "employee")
record.setObject("Jacob", forKey: "firstName")
record.setObject("Nielsen", forKey: "lastName")
database.save(record, completionHandler: { (record: CKRecord?, error: NSError?) -> Void in })
// Fetch record
let recordID = record.recordID
database.fetch(withRecordID: recordID, completionHandler: { (record: CKRecord?, error: NSError?) -> Void in
})
Discussion