オブジェクトの JSON 形式ファイルへのエンコードとデコード
ここでは、オブジェクトを JSON 形式にエンコードしてファイルに保存し、そしてさらに JSON 形式で保存されたデータをデコードすることで、元のオブジェクトを復元する方法を示します。
一般的にはこのように、メモリ上のオブジェクトを不揮発性のストレージに保存し、その保存データからオブジェクトを作り直すような作業はオブジェクトの「永続化」と言われます。
具体的には、ここでは Swift の Person クラスを作ります。 このクラスは "firstName" というプロパティと "lastName" というプロパティを持つことにします。
Person クラスのオブジェクトを、そのまま JSON 形式でファイルに保存し、そして、保存された JSON 形式のファイルから 今度は Person クラスのインスタンスを作る、ということをします。
画面は次のようにします。
テキストフィールドが 2 つあり、文字を入力して Save ボタンをタップします。このとき、テキストフィールドの値から Person オブジェクトを作ります。そして、このオブジェクトを JSON 形式にエンコードしてファイルに保存します。
Load ボタンをタップ、もしくはビューコントローラの viewDidLoad のタイミングで今度は JSON ファイルを読み取り Person オブジェクトを作り直して、その情報からテキストフィールドに値を戻します。
さっそくユーザーインターフェイスを作ります。上部に Save ボタンと Load ボタンを配置。そして、中段にテキストフィールドを二つ並べます。
そして、下段に URL 名 (ファイル名) とファイルの内容を表示するラベルを配置します。
アウトレットの関連付けは上のスクリーンショットの通りです。
ビューコントローラ ViewController.swift は次のようになります。
import UIKit class ViewController: UIViewController { @IBOutlet weak var firstNameTextField: UITextField! @IBOutlet weak var lastNameTextField: UITextField! @IBOutlet weak var urlLabel: UILabel! @IBOutlet weak var dataLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() loadData() } @IBAction func saveTapped(_ sender: Any) { guard let fname = firstNameTextField.text, let lname = lastNameTextField.text else { return } try? PersonStore.shared.save(person: Person(fname,lname)) showRawData() } @IBAction func loadTapped(_ sender: Any) { loadData() print(Person.self) } func loadData(){ guard let p = PersonStore.shared.read() else { return } firstNameTextField.text = p.firstName lastNameTextField.text = p.lastName showRawData() } func showRawData(){ urlLabel.text = PersonStore.shared.dataUrl.absoluteString if let txt = try? String( contentsOf: PersonStore.shared.dataUrl) { dataLabel.text = txt } } }
saveTapped 関数と loadTapped はそれぞれ、Save ボタン、Load ボタンのアクションになっています。つまり、それぞれのボタンをタップしたときに、実行される関数です。
ここでは、自前の Person クラスの保存や読み込みを受け持つ、PersonStore というクラスを用意して、 そこで Person オブジェクトの保存や読み込みをします。
オブジェクトから JSON 形式へのエンコード
オブジェクトを JSON 形式へエンコードする流れは、次のようになります。
- エンコードするクラスは Codable プロトコルをコンフォームする。
- JSONEncoder の encode(_:) メソッドにオブジェクトを渡して、 Data オブジェクトを取得する。
- Data の write(to:) メソッドにファイルの URL を渡して書き込む。
これを頭に入れつつ、読み進んでください。
それでは、具体的に Person クラスを実装しましょう。
Person.swiftは次のようにします。
import Foundation class Person : Codable { var firstName: String var lastName: String init(_ firstName: String, _ lastName: String) { self.firstName = firstName self.lastName = lastName } }
JSON 形式へのエンコード、デコードをするために Codable プロトコルをコンフォームする必要があります。
Codable は Encodable と Decodable プロトコルのタイプエイリアスです。
その他は単に、プロパティ 2 つと イニシャライザを記述しているだけです。
次の PersonStore.swift で Person オブジェクトを JSON 形式でファイルに保存したり、JSON 形式のファイルを読み込んで、Person オブジェクトを作り直したりしています。
import Foundation final class PersonStore { static let shared = PersonStore() private init() {} let dataUrl: URL = { let url = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask).first! let dataUrl = url.appendingPathComponent("data.json") return dataUrl }() func save(person p:Person) throws { let data = try JSONEncoder().encode(p) try data.write(to: dataUrl) } func read() - > Person? { if let data = try? Data(contentsOf: dataUrl), let p = try? JSONDecoder().decode(Person.self, from: data) { return p } return nil } }
この PersonStore クラスはシングルトンとして実装しています。Swift でシングルトンを実装するポイントは 3 つあって、 1つ目はクラスを final とすること。2つ目はスタティック・プロパティ (default とか shared という名前がよく使われます) にオブジェクトをセットしておくこと、 3つ目はイニシャライザ init を private とすることです。
このクラスでは、dataUrl という名前で、JSON ファイルの URL (パス) をとれるようにしています。
save(person:) メソッドでは Person オブジェクトを受け取り、それを JSON 文字列にエンコードしています。
func save(person p:Person) throws { let data = try JSONEncoder().encode(p) try data.write(to: dataUrl) }
JSONEncoder は Foundation フレームワークのクラスで、エンコード可能なオブジェクトを受け取り、それを JSON 形式にエンコードします。(エンコード可能なオブジェクトというのは、 Encodable プロトコルをコンフォームしているクラスのオブジェクトです。ここで Person クラスは Codable でしたので、エンコード・デコード両方可能です )
JSONEncoder クラスの encode(_:) メソッドで JSON 形式にエンコードし、結果は Data オブジェクトとして返ります。Data クラスは Foundation フレームワークのクラスで、メモリ上のバイトバッファを表します。
Data オブジェクトの write(to:options:) メソッドに書き込み先のファイルの URL を渡すと、バッファの内容がファイルに書き込まれます。(options のデフォルト値は [] なので何も渡さなくても OK です)
尚、encode(_:) も write(to:options:) でもどちらも、失敗する場合にはエラーが投げられますので、try 付きで呼んでいます。エラーはキャッチしていないので、このメソッドはエラーを投げる (throw する可能性) ことを想定します。
C++ や Java などでは try - catch で処理されるのは「例外」処理ですが、Swift では仕組みが異なるので例外処理と区別されて「エラー」処理といいます。
以上で、オブジェクトから JSON 形式にエンコードして、ファイルに保存できました。
JSON 形式からオブジェクトへのデコード
次に JSON 形式からオブジェクトへ、デコードしてオブジェクトを再現しましょう。
JSON 形式からオブジェクトへデコードする流れは、次のようになります。
- デコードするクラスは Codable プロトコルをコンフォームする。
- JSONDecoder の decode(_:from:) メソッドを、オブジェクトとなるクラスのメタデータ (Class名.self) と JSON データを Data オブジェクトに読み込んだものを渡して呼び出す。
これを頭に入れつつ、読み進んでください。
JSON 形式でフォーマットされたファイルを読み込み、Person オブジェクトを再構築するのは次のようにします。
func read() -> Person? { if let data = try? Data(contentsOf: dataUrl), let p = try? JSONDecoder().decode(Person.self, from: data) { return p } else { return nil }
まず Data クラスのイニシャライザ init(contentsOf:options:) にファイルの URL を渡すと、 その URL の内容が Data オブジェクトに読み込まれます。
次に JSONDecoder の decode(_:from:) メソッドを、JSON データから元に戻したいクラスのクラス情報 (クラス名.self で取れます) と Data オブジェクトを渡して呼び出します。これだけで、上手くいけば オブジェクトが再生されて作られます。
データ型が全く違うなどして再構築できない場合は、try? で呼んでいるので nil が返ります。