忘備録-iOSアプリの永続的なデータ保存

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

## 参考文献

この記事は以下の書籍の情報を参考にして執筆しました。5章を読んでのメモになります。

- [本気ではじめるiPhoneアプリ作り Xcode 10.x対応 (Informatics&IDEA)](https://www.amazon.co.jp/%E6%9C%AC%E6%B0%97%E3%81%A7%E3%81%AF%E3%81%98%E3%82%81%E3%82%8BiPhone%E3%82%A2%E3%83%97%E3%83%AA%E4%BD%9C%E3%82%8A-Xcode-10-x%E5%AF%BE%E5%BF%9C-Informatics-IDEA/dp/4815600031/)




##iOSデバイスのデータ保存場所

[appleのページ](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/d5dc418a-74ab-aa39-78e6-df5f7649a230.png)


|ローカルストレージのパス|内容|バックアップ|
|---|---|---|
＜AppName＞.app|アプリ本体そのもの。|されない
Documents/|ユーザーが生成したデータの保存先に利用される。共有することができるから公開したいデータの保存に使用するといい。|される
Documents/Inbox|他のアプリからのデータを受け取るためのディレクトリ。読み取り専用。|されない
Library|ユーザのデータ(個人情報とか)を除いたデータのためのディレクトリ。このディレクトリの下にサブディレクトリを作って利用する。|Cachesのみされない
Library/Caches|高速にアクセスするためのデータを一時的にキャッシュして配置するディレクトリ。アプリ利用中でなければ、ディスクの容量が少なくなった時に削除される可能性がある。|されない
Library/Preference|UserDefaultsを格納するディレクトリ|される
Library/＜カスタムディレクトリ＞|Libraryの下にはカスタムディレクトリを作成することが可能で、ユーザーに晒したくないデータを配置することができる。|される
tmp|アプリ利用中にメモリに持ち続ける必要のない一時できなデータを配置できるディレクトリ。アプリの利用を止めた時にiOSがデータを破棄する場合あり。|されない


##データを扱う仕組み

###UserDefaults
「key=value」形式でデータを保存。
保存したデータはplist(プロパティリスト)形式で保存される。plistの実体はXML形式。

・メリット
UserDefaultsをプログラムが扱う際は、ファイルの内容をメモリに読み込んで参照するため高速。(メモリに値を保存して利用する方法を「キャッシュ」という。)

・デメリット
大きすぎるデータを取り扱うと、メモリへのコピーに時間がかかってしまうのでアプリの動作が遅くなる。

・注意点
アプリが予期せぬタイミングで終了してしまうと、メモリからUserDehaultsにデータが保存されないため、大事なデータは明示的に保存する処理を検討する必要がある。




##Core Data
レコード形式でデータを保存。(テーブルに保存するイメージ)
RDB(Relational DataBase)を構築することができる。

・メリット
大量のデータ保存に向いている。

・デメリット
利用方法が複雑で学習コストが高い。

小容量のデータはUserDefaults、大容量のデータはCore Dataに分けて使うなど使い分けを検討する。

##iCloud
「key=value」形式、Core Data形式、ドキュメント形式、構造化データ形式(レコード単位のデータ)でデータを保存。
アプリが保存しているデータをサーバ上に配置し、ネットワークで繋がっている端末同士でデータを共有できる機能。

・メリット
異なるデバイス間でデータを共有できる。


##Keychain
「key=value」形式でデータを保存。
iOS内部のセキュアな場所に保存される仕組み。
他のアプリからは参照できない。保存時のアクセス設定によっては同一開発者のアプリ同士であれば参照・編集が可能。
アプリを削除しても、デバイスにデータが残る。
アプリの再インストールを行うとまたアクセスできる。

・メリット
機密性の高いデータを保存できる。

・デメリット
大量のデータ保存には向かない。


##メモ : 大量のデータの扱いにOSSを考える
手軽に大容量のデータを扱える選択肢としてOSSの活用がある。
※2018/11/22の本の内容です。

[Realm](https://realm.io/jp)
Core Dataよりも直感的に利用でき、動作も高速

[MagicalRecord](https://github.com/magicalpanda/MagicalRecord)
Core Dataをより簡単に利用できる。
MagicalRecordを通してCoreDataを利用するイメージ。
Object-Cで記述されているのでSwiftとの互換性を使いながら利用する。

##UserDefaultsに独自クラスを追加する
UserDefaultsに使える型
Any型の配列型、真偽値型、データ型、辞書型、浮動小数点数型、整数型、オブジェクト型、文字列型、文字列型の配列、URL、Double型

例えば画面表示時に独自クラスをUserDefaultsに格納しようとしてみる。

```swift:BuyObject.swift
class BuyObject {
  let name: String
  let quantity: Double
  
  init(name n: String, quantity q: Double = 1){
    name = n
    quantity = q
  }
}
```

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    let userDefaults = UserDefaults.standard
    let hoge = BuyObject(name: "hoge")
    
    userDefaults.set(hoge, forKey: "hoge")
    userDefaults.synchronize()
  }
}
```

実行するとアプリ起動直後にクラッシュする。
エラーメッセージ : Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object ShoppingList.BuyObject for key hoge'
UserDefaultsが対応していない型で値をセットしようとしたためエラーとなる。

###シリアライズとデシリアライズ
UserDefaultsには基本的な方を保存する仕組みがあるが、独自クラスを保存したい場合、一旦Data型に変換して保存する。
この変換を「シリアライズ」という。シリアライズされたデータを復元することを「デシリアライズ」という。

シリアライズ、デシリアライズはそれぞれ、NSKeyedArchiverクラスとNSKeyedUnarchiverクラスに処理がある。
処理自体はNSSecureCodingの継承元のNSCodingプロトコルのメソッドが呼び出されている。

NSSecureCodingプロトコルのメソッドを実装する。
注意点としてNSCoder.decode~メソッドには種類があって型によって変えないといけないので注意。

```swift:BuyObject
import Foundation    //追加

class BuyObject:NSObject, NSSecureCoding {    // プロトコル追加

  let name: String
  let quantity: Double
  static var supportsSecureCoding: Bool{ return true }    // プロトコル適応に必須メンバ

  init(name n: String, quantity q: Double){
    name = n
    quantity = q
  }
  
  // 以下プロトコル適応に必須メソッド
  
  required init?(coder aDecoder: NSCoder){
    name = aDecoder.decodeObject(forKey: "name") as! String
    quantity = aDecoder.decodeDouble(forKey: "quantity")    // 注意 : 型によって使うメソッドが変わる
  }

  func encode(with aEncoder: NSCoder) {
    aEncoder.encode(name, forKey: "name")
    aEncoder.encode(quantity, forKey: "quantity")
  }

}
```
ViewControllerではUserDefaultsに値が入っていれば表示し、UserDefaultsに値を入れる処理を実装した。
シミュレータでアプリを削除後に起動後は何もprintされないが、アプリを実行後もう1どプログラムを実行すると、値が表示される。

```swift:ViewController.swift

import UIKit

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    let hoge = BuyObject(name: "hoge", quantity: 1)
    let key = "hoge"
    
    if let buyObject = Deserialize(userDefaultsKey: key){
      print(buyObject.name, buyObject.quantity)    // hoge 1.0
    }

    Serialize(data: hoge, userDefaultsKey: key)
  }
  
  func Serialize(data: BuyObject, userDefaultsKey: String){
    let userDefaults = UserDefaults.standard
    
    do {
      // シリアライズ処理
      let archiveData = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
      userDefaults.set(archiveData, forKey: userDefaultsKey)
      userDefaults.synchronize()
    } catch {
      print("error")
    }
  }
  
  func Deserialize(userDefaultsKey: String) -> BuyObject?{
    let userDefaults = UserDefaults.standard
    
    guard let storedData = userDefaults.object(forKey: userDefaultsKey) as? Data else { return nil }
    do{
      // デシリアライズ処理
      if let unarchivedData = try NSKeyedUnarchiver.unarchivedObject(ofClass: BuyObject.self, from: storedData){
        return unarchivedData
      }
    } catch {
      print("error")
    }
    return nil
  }
  
}
```