【part 9】AppApp のリファクタリング過程紹介【Realm編①】

どうも。Reoです。

今回からゴリゴリコード修正をしていきます。

今までのパートは、記事にかける時間が9割ぐらいで、実際の作業が1割ぐらいでした。今回からは、作業する方の割合が高くなる予定です。コードをゴリゴリ修正していきます。

修正した先が上手く書けているのか、自信は全くないです。でもこの自信は何年経ってもないだろうから、書いていきます。

 

お久しぶりになってしまったんですが、この記事を書き始めたのは2週間くらい前です。地味にコツコツ書いてたんですが、修正箇所が多すぎてなかなか厳しいです。

あんまり役に立つ記事ではないと思うので、適当に日記を読むぐらいの感覚で読んでください。

 

環境

  • Xcode11.3.1
  • Swift 5.1.3
  • iOS13.3
  1.  AppApp のリファクタリングを始めます!【SwiftLint 導入編】
  2. 【part 2】AppApp のリファクタリング過程紹介【SwiftLint導入後のError解消編】
  3. 【part 3】AppApp のリファクタリング過程紹介【SwiftLint導入後のWarning解消編】
  4. 【part 4】AppApp のリファクタリング過程紹介【コーディング規約編】
  5. 【part 5】AppApp のリファクタリング過程紹介【ディレクトリ構成編】
  6. 【part 6】AppApp のリファクタリング過程紹介【R.swift導入編】
  7. 【part 7】AppApp のリファクタリング過程紹介【画像・色の管理編】
  8. 【part 8】AppApp のリファクタリング過程紹介【Carthage/LicensePlist編】

 

リポジトリ→ uruly/AppApp

 

今回の目標

Realmのオブジェクトを扱いやすくわかりやすくします。頑張ります。

本当に今現在が酷いコードなので、どうにかしたいです。UIとかをいじる前に手をつけておきたい。

 

なぜRealm?

まず、ローカルにデータを保存したい場合の選択肢は以下のようなものではないでしょうか。

  1. UIDocument を使う
  2. CoreData
  3. Realm

UIDocument でやるのは結構しんどいので、CoreData か Relam を使いたいと考えました。そもそも AppApp で Realm を選択したのは、CoreData より扱いやすそうだったからです。

むしろ今となると、CoreData は、個人的に使うのが怖いです。トラウマです。(詳細は、iOS12でCoreDataのExternal Storageを利用しているデータが破損してしまう問題が発生した件を読んでください。)

今のところRealmで痛い目には合ってないので…

ただ、ぼちぼち4つ目の選択肢を考えるべきなのかなとも思います。

Firestoreで作るオフライン動作対応の iOSアプリ – mono  – Medium

 

今回は、Realmのままやっていきますが、今後新規プロジェクトを作る際には脳死でRealmを選ぶのはやめて、都度ちゃんと合ったものを選択していきたいです。

 

これまでのコード

いやだ。見せたくない。イヤダーーーーーーーーーッ

 

修正前のラベル部分のコードです。飛ばしてください。

class AppLabelRealmData: Object {
    @objc dynamic var name: String?      //ラベルの名前
    @objc dynamic var color: Data?    //ラベルの色
    @objc dynamic var id: String?        //id
    @objc dynamic var order = 0     //順番
    @objc dynamic var explain: String?

    override static func primaryKey() -> String? {
        return "id"
    }
}

struct AppLabelData {
    var name: String!
    var color: UIColor!
    var id: String!
    var order: Int!
    var explain: String?
}

class AppLabel {
    //全てのラベルデータを入れる
    var array: [AppLabelData] = [] {
        didSet {
            AppLabel.count = array.count
        }
    }

    //現在のラベル
    static var currentID: String?
    static var currentOrder: Int?
    static var count: Int?
    static var currentColor: UIColor?
    static var currentBackgroundColor: UIColor? {
        didSet {
            let userDefaults = UserDefaults.standard
            if let color = AppLabel.currentBackgroundColor {
                let data = NSKeyedArchiver.archivedData(withRootObject: color)
                userDefaults.set(data, forKey: "backgroundColor")
            } else {
                userDefaults.removeObject(forKey: "backgroundColor")
            }

        }
    }
    static var currentBackgroundImage: UIImage? {
        didSet {
            let userDefaults = UserDefaults.standard
            if let image = AppLabel.currentBackgroundImage {
                let data = image.pngData()
                userDefaults.set(data, forKey: "backgroundImage")
            } else {
                userDefaults.removeObject(forKey: "backgroundImage")
            }

        }
    }

    init() {
        self.migration()
        self.reloadLabelData()
        if array.count == 0 {
            saveDefaultData()
        }
        let userDefaults = UserDefaults.standard
        if let colorData = userDefaults.data(forKey: "backgroundColor") {
            AppLabel.currentBackgroundColor = NSKeyedUnarchiver.unarchiveObject(with: colorData) as? UIColor
        }
        if let backImageData = userDefaults.data(forKey: "backgroundImage") {
            AppLabel.currentBackgroundImage = UIImage(data: backImageData )
        }
    }

    func migration() {
        var config =  Realm.Configuration(
            schemaVersion: .schemaVersion,
            migrationBlock: { migration, oldSchemaVersion in
                //print(oldSchemaVersion)
                if oldSchemaVersion < 4 { migration.enumerateObjects(ofType: AppRealmData.className()) { _, newObject in //print("migration") newObject!["urlString"] = "" } migration.enumerateObjects(ofType: AppLabelRealmData.className()) { _, newObject in newObject!["explain"] = "" } } }) let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.xyz.uruly.appapp")! config.fileURL = url.appendingPathComponent("db.realm") Realm.Configuration.defaultConfiguration = config } func reloadLabelData() { // 略 } //一番最初に呼ばれる予定のデータ。Allが入る func saveDefaultData() { // 略 } static func saveLabelData(name: String, color: UIColor, id: String, order: Int, explain: String?, _ completion:() -> Void) {
        // 略
    }

    static func updateLabelData(name: String, color: UIColor, id: String, order: Int, explain: String?, _ completion:() -> Void) {
        // 略
    }

    static func contains(name: String) -> Bool {
        // 略
        return false
    }

    static func contains(color: UIColor, isEdit: Bool, id: String) -> Bool {
        // 略
        return false
    }

    static func deleteLabelData(labelID: String, _ completion:() -> Void) {
        // 略
    }

    //並び順を更新
    func resetOrder() {
        // 略
    }
}

長い! わからん! 関数の中身省略しても長い!

 

修正方法を考える

どうやって修正していこう…

前提として、今あるデータを消したくないのでマイグレーションをしないといけない。そうじゃなかったらどうとでもなりそうだけれど…。

現在は、Object は完全に保存用として、使うときは別途構造体に入れて使っているみたいです。

Realm の Object はクラスなので、それを struct にして使うことはやってる人はいるっぽいですね。

How to use struct with Realm ? – Ludovic Jamet – Medium

うーん。このまま行くか、クラスに統一するか…うーん。うーん。わからん。

わからんので、ちょっと、GitHubで import RealmSwift で検索して勉強してきます。

 

うーん。違うなー。まず何がダメなのか考えよう…

  • 1つのファイルにclassを2つ書いてるのは分けたい
  • マイグレーション処理が複数回出てくる。つらい。
  • 一つ一つの関数が重い(省略したけど30行ずつぐらいある…)
  • 名付けがわかりづらい…
  • オプショナルである必要があるのかどうか

 

どうやら Object クラスのものは全部で3つあるらしいです。

  • AppRealmData
  • ApplicationData
  • AppLabelRealmData

こんなわからん名前ある?ってぐらいわからんですね。

とりあえず、一度修正作業してみようと思います。

 

ファイルを分ける

とにかく見通しが悪いので、まずはファイルを分けます。名前はマイグレーション処理がいるはずなので、まだ変えません。

それぞれ上記のクラスに合わせてファイルを作成しました。

 

構造体をなくす

現状は以下のように、構造体をそれぞれのObjectに合わせて用意しています。

  • AppRealmData – AppStruct
  • ApplicationData – ApplicationStruct
  • AppLabelRealmData – AppLabelData

名前の統一性のなさよ…本当にもう…

とりあえずこの構造体部分をなくそうと思います。

作業してきます。作業Doneしたので次いきます。

 

DatabaseManager を作ってしまった

マイグレーション処理とスキーマの指定を何度もやっているのをやめたいので、

現状ではRealmを使うのに毎度以下のようにやっています。

var config = Realm.Configuration(schemaVersion: .schemaVersion)
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.xyz.uruly.appapp")!
config.fileURL = url.appendingPathComponent("db.realm")

let realm = try! Realm(configuration: config)

何度も書きたくないです。(グローバルな奴に設定しておけば良いので本当は一回でいいのかも)

先ほど調べていたときに見つけた blkbrds/rxswift-tutorialsDatabaseManager.swift を参考にさせてもらいました。

import Foundation
import RealmSwift

final class DatabaseManager {

    static let shared = DatabaseManager()

    private let schemaVersion: UInt64 = 5

    private var realm: Realm {
        do {
            return try Realm(configuration: configuration)
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    private lazy var configuration: Realm.Configuration = {
        var configuration = Realm.Configuration(schemaVersion: .schemaVersion, migrationBlock: migrationBlock)
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: .groupID)
        configuration.fileURL = url?.appendingPathComponent("db.realm")

        return configuration
    }()

    private lazy var migrationBlock: RealmSwift.MigrationBlock? = {
        return .init { (migration, schema) in
            // マイグレーション処理
        }
    }()
}

あんまりシングルトンでやらないので、どうなんだろう。

 

取得処理等もextensionで。(ほぼほぼ DatabaseManager.swift を真似させてもらっています。)

 

extension DatabaseManager {

    // MARK: - Fetch

    func object(_ type: T.Type, filter predicate: NSPredicate? = nil) -> T? {
        var results: Results
        if let predicate = predicate {
            results = realm.objects(type).filter(predicate)
        } else {
            results = realm.objects(type)
        }

        return results.isEmpty ? nil : results.first
    }

    // 以下略
}

これで以下のように取得できるようになりました。

let sortProperties = [SortDescriptor(keyPath: "order", ascending: true)]
let objects = DatabaseManager.shared.objects(AppLabelRealmData.self, filter: nil, sortedBy: sortProperties)

 

オプショナルを考える

現状のオブジェクトを見てみます。

final class AppRealmData: Object {

    @objc dynamic var name: String!      //アプリの名前
    @objc dynamic var developer: String!    //デベロッパ名
    @objc dynamic var id: String!        //アプリのid
    @objc dynamic var urlString: String!    //url
    @objc dynamic var image: Data!   //アイコンの画像
    @objc dynamic var date: Date!    //アプリを登録した日付

    override static func primaryKey() -> String? {
        return "id"
    }
}

まず Implicitly Unwrapped Optional である!をつける必要があるのかどうか。

少し古い記事ですが、以下をちょっと改めて読みました。

暗黙的アンラップ型(Implicitly Unwrapped Optional)をいつ使うべきか? – Qiita

ほいで。初期値を与えられるものは与えて、任意の項目はオプショナルにしました。

final class AppRealmData: Object {

    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var developer: String = ""
    @objc dynamic var urlString: String = ""
    @objc dynamic var image: Data?
    @objc dynamic var date: Date = Date()

    override static func primaryKey() -> String? {
        return "id"
    }
}

アッ…

この変更はマイグレーションが必要なようです…

 

次回

あ、諦めたんじゃないッ!!

次回こそ、マイグレーション処理等をしてRealm編を終わらせます。

その次ぐらいでiOS13でアプリの保存をできるようにしたいです。

 

おわりに

2週間ぐらいに渡って書いているんですが(触ってるのは3日ほど)、なかなか厳しいですね。

今回で保存したアプリの情報も見られない状態になってしまっています。

できる限り早く使える子にしたいですが、この子がお金になる予定はないはず(このブログの記事を見てもらうぐらい)なので、優先度を上げすぎるのも良くないなと思っています。

なので、まったりと直していきますね。

 

最近個人でやることもちゃんと計画を立ててやろうと思って、ガントチャートを壁に貼りました。今日はこのブログを書くのが最優先にしてあったので、書いています。

4月からの忙しさがまだわからないのですが、今月はあと記事2本分の作業を行う予定です。

 

ではではまた次回。

 

AppApp ,
コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS
more