[SwiftUI]RealmのデータをJSON形式で入出力する方法

SwiftUIでRealmのデータをJSONファイルで入出力する方法に困ったことはありませんか?

ネットで調べても情報が少なく、私も苦戦しました。

そこで、コーディング方法についてまとめましたので、ぜひ参考にしてください。

RealmデータをJSONファイルで出力する方法

JSONオブジェクトへエンコード

JSONEncoderクラスを使うことでデータ型のインスタンスをJSONオブジェクトへ変換することができます。

Realmのデータを使ってデータ型のインスタンス(Codableプロトコルに準拠している構造体)を生成します。

// データ型のインスタンスをJSONオブジェクトとしてエンコードするオブジェクトを設定します。
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
        
// 各モデルのバックアップするデータを取得します。
let items = Array(Item.all())
let users = Array(User.all())
let companies = Array(Company.all())

// バックアップするデータからデータ型のインスタンスを生成します。
let jsonData = JsonData(items: items, users: users, companies: companies)

// データ型のインスタンスをJSONオブジェクトとしてエンコードします。
if let data = try? encoder.encode(jsonData) {
    return data
}

Codableプロトコルに準拠している構造体

// バックアップデータをJSONオブジェクトへエンコードする際に使用するインスタンスの型です。
// バックアップするモデルを指定してください。
struct JsonData: Codable {
    var items: [Item]
    var users: [User]
    var companies: [Company]
}

JSONオブジェクトを生成

エンコードで変換したJSONオブジェクトからFileDocumentプロトコルに準拠するクラスのインスタンスを生成します。

// JSONFileを作成します。
self.jsonFile = TextFile(initialText: String(bytes: jsonData, encoding: .utf8)!)

// FileDocumentプロトコルに準拠するクラス
struct TextFile: FileDocument {
    var text = ""

    // ファイルを新規作成する場合に使用します。
    init(initialText: String = "") {
        text = initialText
    }
}

JSONファイルの出力画面を表示

fileExporterでJSONファイル出力用の画面を表示します。

ユーザは画面でファイル名と出力箇所を選択して、JSONファイルを出力することができるようになります。

.fileExporter(
    isPresented: self.$isExporting,    // trueにしたときにファイル出力用の画面が表示されます。
    document: self.jsonFile,           // 出力するファイルを指定します。
    contentType: .plainText,           // 出力するファイルの種類を指定します。
    defaultFilename: self.jsonFileName // デフォルトのファイル名を指定します。ファイル名は画面で変更できます。
) { result in
    if case .success = result {
        // ファイルの出力に成功した時の処理
    } else {
        // ファイルの出力に失敗した時の処理
    }
  }

RealmデータをJSONファイルで入力する方法

JSONファイルの入力画面を表示

fileImporterでJSONファイル入力用の画面を表示します。

ユーザは画面で入力するJSONファイルを選択することができるようになります。

.fileImporter(
    isPresented: $isImporting,         // trueにしたときにファイル取込用の画面が表示されます。
    allowedContentTypes: [.plainText], // 取込可能なファイルの種類を指定します。
    allowsMultipleSelection: false     // ユーザーが複数のファイルを同時に選択できるか指定します。
) { result in
    do {
        // ファイルを読み込み、Realmに書き込みます。
        guard let selectedFile: URL = try result.get().first else { return }
        RealmUtils.restore(jsonValue: try Data(contentsOf: selectedFile))
    } catch {
        // ファイルの取込に失敗した時の処理
    }
}

JSONオブジェクトをデコード

JSONDecoderクラスを使うことでJSONオブジェクトからデータ型のインスタンスへ変換することができます。

JSONオブジェクトは画面から選択したJSONファイルです。

データ型のインスタンスの構造体はエンコードと同じものを使います。

// JSONオブジェクトからデータ型のインスタンスをデコードするオブジェクトを設定します。
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

// JSONオブジェクトからデータ型のインスタンスをデコードします。
guard let jsonData: JsonData = try? decoder.decode(JsonData.self, from: jsonValue) else {
    fatalError("Failed to decode from JSON.")
}

JSONオブジェクトを読み込む

デコードで変換したデータ型のインスタンスをRealmへ書き込みます。

別オブジェクトを参照しているモデルの場合は、データ1件ずつ参照先のリストを作成する必要があるので要注意です。

let realm = try! Realm()

// データ型のインスタンスをRealmへ書き込みます。
try! realm.write {
    
    // 重複エラーを防ぐために既存のデータを削除します。
    realm.deleteAll()
   
    // JSONデータを読み込み、モデルごとにRealmへ書き込みます。
    realm.add(jsonData.items)
    realm.add(jsonData.companies)

    // 1対多、多対多などのリレーションを持ち、別オブジェクトを参照している場合は、
    // 参照先のリストを作成し1件ずつ書き込む必要があります。
    for user in jsonData.users {
        let userTemp = User()
        userTemp.id = user.id
        for company in user.companies {
            userTemp.companies.append(Company.get(id: company.id).first!)
        }
        realm.add(userTemp)
    }
            
}

サンプル

説明で用いたサンプルの全てのプログラムを載せておきます。

参考にしてください。

import SwiftUI
import RealmSwift

struct ContentView: View {
    
    @State private var jsonFile: TextFile? = nil
    @State private var jsonFileName: String = ""
    @State private var isImporting: Bool = false
    @State private var isExporting: Bool = false
    @State private var alertItem: AlertItem? = nil
    
    struct AlertItem: Identifiable {
        var id = UUID()
        var alert: Alert
    }
    
    var body: some View {
        VStack {
            Button(action: {
                // バックアップデータを取得します。
                if let jsonData: Data = RealmUtils.backup() {         
                    // JSONFileを作成します。
                    self.jsonFile = TextFile(initialText: String(bytes: jsonData, encoding: .utf8)!)
                    
                    // File名を指定します。ここではファイル名の後ろにシステム日付をつけます。
                    let dateformater = DateFormatter()
                    dateformater.dateFormat = "yyyyMMddhhmmss"
                    dateformater.locale = Locale(identifier: "ja_JP")
                    self.jsonFileName = "Backup_" + dateformater.string(from: Date())
                    
                    // ファイル出力用の画面を表示するためのフラグをtrueにします。
                    self.isExporting = true
                }
            }){
                Text("バックアップを作成")
            }
            Button(action: {
                isImporting = true
            }){
                Text("バックアップから復元")
            }
        }.fileExporter(
            isPresented: self.$isExporting,    // trueにしたときにファイル出力用の画面が表示されます。
            document: self.jsonFile,           // 出力するファイルを指定します。
            contentType: .plainText,           // 出力するファイルの種類を指定します。
            defaultFilename: self.jsonFileName // デフォルトのファイル名を指定します。ファイル名は画面で変更できます。
        ) { result in
            if case .success = result {
                // ファイルの出力に成功した時のメッセージを指定します。
                alertItem = AlertItem(alert: Alert(title: Text("成功"),
                      message: Text("作成できました。"),
                      dismissButton: .default(Text("OK"))))
            } else {
                // ファイルの出力に失敗した時のメッセージを指定します。
                alertItem = AlertItem(alert: Alert(title: Text("失敗"),
                      message: Text("作成できませんでした。"),
                      dismissButton: .default(Text("OK"))))
            }
        }.fileImporter(
            isPresented: $isImporting,         // trueにしたときにファイル取込用の画面が表示されます。
            allowedContentTypes: [.plainText], // 取込可能なファイルの種類を指定します。
            allowsMultipleSelection: false     // ユーザーが複数のファイルを同時に選択できるか指定します。
        ) { result in
            do {
                // ファイルを読み込み、Realmに書き込みます。
                guard let selectedFile: URL = try result.get().first else { return }
                RealmUtils.restore(jsonValue: try Data(contentsOf: selectedFile))
                // ファイルの取込に成功した時のメッセージを指定します。
                alertItem = AlertItem(alert: Alert(title: Text("成功"),
                      message: Text("復元できました。"),
                      dismissButton: .default(Text("OK"))))
            } catch {
                // ファイルの取込に失敗した時のメッセージを指定します。
                alertItem = AlertItem(alert: Alert(title: Text("失敗"),
                      message: Text("復元できませんでした。"),
                      dismissButton: .default(Text("OK"))))
            }
        }.alert(item: $alertItem) { item in
            item.alert
        }
    }
}
import SwiftUI
import UniformTypeIdentifiers

struct TextFile: FileDocument {
    // UTType.plainTextのみ読み込み可能であることを明示します。
    static var readableContentTypes = [UTType.plainText]

    var text = ""

    // ファイルを新規作成する場合に使用します。
    init(initialText: String = "") {
        text = initialText
    }

    // すでに存在するファイルを読み込む場合に使用します。
    init(configuration: ReadConfiguration) throws {
        if let data = configuration.file.regularFileContents {
            text = String(decoding: data, as: UTF8.self)
        }
    }

    // ファイルを書き込む場合に使用します。
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let data = Data(text.utf8)
        return FileWrapper(regularFileWithContents: data)
    }
}
import RealmSwift

// バックアップデータをJSONオブジェクトへエンコードする際に使用するインスタンスの型です。
// バックアップするモデルを指定してください。
struct JsonData: Codable {
    var items: [Item]
    var users: [User]
    var companies: [Company]
}

class RealmUtils {
    
    // バックアップデータの取得
    public static func backup() -> Data? {
        
        // データ型のインスタンスをJSONオブジェクトとしてエンコードするオブジェクトを設定します。
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        encoder.dateEncodingStrategy = .iso8601
        
        // 各モデルのバックアップするデータを取得します。
        let items = Array(Item.all())
        let users = Array(User.all())
        let companies = Array(Company.all())
        
        // バックアップするデータからデータ型のインスタンスを生成します。
        let jsonData = JsonData(items: items, users: users, companies: companies)
        
        // データ型のインスタンスをJSONオブジェクトとしてエンコードします。
        if let data = try? encoder.encode(jsonData) {
            return data
        }
        return nil
    }
    
    // JSONファイルの読み込み
    public static func restore(jsonValue: Data) {
        
        let realm = try! Realm()
        
        // JSONオブジェクトからデータ型のインスタンスをデコードするオブジェクトを設定します。
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        
        // JSONオブジェクトからデータ型のインスタンスをデコードします。
        guard let jsonData: JsonData = try? decoder.decode(JsonData.self, from: jsonValue) else {
            fatalError("Failed to decode from JSON.")
        }
        
        // データ型のインスタンスをRealmへ書き込みます。
        try! realm.write {
            
            // 重複エラーを防ぐために既存のデータを削除します。
            realm.deleteAll()
            
            // JSONデータを読み込み、モデルごとにRealmへ書き込みます。
            realm.add(jsonData.items)
            realm.add(jsonData.companies)

            // 1対多、多対多などのリレーションを持ち、別オブジェクトを参照している場合は、
            // 参照先のリストを作成し1件ずつ書き込む必要があります。
            for user in jsonData.users {
                let userTemp = User()
                userTemp.id = user.id
                for company in user.companies {
                    userTemp.companies.append(Company.get(id: company.id).first!)
                }
                realm.add(userTemp)
            }
            
        }
        
    }
    
}
import RealmSwift

final class Item: Object, ObjectKeyIdentifiable, Identifiable, Codable {
    
    @objc dynamic var id: Int = 0
    @objc dynamic var name: String = ""

    override static func primaryKey() -> String? {
        return "id"
    }
    
    // 全データを取得する。
    static func all() -> Results<Item> {
        let realm: Realm = try! Realm()
        return realm.objects(Item.self)
    }
    
}
import RealmSwift

final class User: Object, ObjectKeyIdentifiable, Identifiable, Codable {
    
    @objc dynamic var id: Int = 0
    @objc dynamic var name: String = ""
    var companies: List<Company> = List<Company>()

    override static func primaryKey() -> String? {
        return "id"
    }
    
    // 全データを取得する。
    static func all() -> Results<User> {
        let realm: Realm = try! Realm()
        return realm.objects(User.self)
    }
    
}
import RealmSwift

final class Company: Object, ObjectKeyIdentifiable, Identifiable, Codable {
    
    @objc dynamic var id: Int = 0
    @objc dynamic var name: String = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    // 全データを取得する。
    static func all() -> Results<Company> {
        let realm: Realm = try! Realm()
        return realm.objects(Company.self)
    }
    
    // idを指定してデータを取得する。
    static func get(id: Int) -> Results<Company> {
        let realm: Realm = try! Realm()
        return realm.objects(Company.self).filter("id == %@", id)
    }
    
}