オブジェクトの 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 形式へエンコードする流れは、次のようになります。

  1. エンコードするクラスは Codable プロトコルをコンフォームする。
  2. JSONEncoder の encode(_:) メソッドにオブジェクトを渡して、 Data オブジェクトを取得する。
  3. 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 形式からオブジェクトへデコードする流れは、次のようになります。

  1. デコードするクラスは Codable プロトコルをコンフォームする。
  2. 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 が返ります。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Swift による iOS 開発入門