Open43

FirestoreからSwiftDataへ移行する作業ログ

satomilkysatomilky

ここの宣言から始めることになりましたSwiftDataへの移行
https://zenn.dev/satomilky/articles/55c21b5e08751a

まずは環境を準備。Xcode15 betaをインストールできてないので、ダウンロードしてインストールしようとしたら、

macOSアップデートできてなかったので、急いでインストール

その間にこの辺りを読んでおく。
https://studist.tech/ios-17-todo-list-app-with-swiftdata-3ffef1311d11
https://zenn.dev/arsaga/articles/cb67b2265b4adf

satomilkysatomilky

ようやくFirestoreと仲良くできた。と思ってたんだけど、今になって捨てる決断することになったのは感慨深い。
単なる個人ツールアプリとしてのデータを取扱うのに、結局Firestoreだとオーバースペックだったんだよね。
別のアプリ開発で、コレらの経験はいかそう。FirestoreのようなNoSQLのモデリングのコツも掴んだので、学びだ。

Firestoreがどこまで技術として生き延びるのか知らんけど。

satomilkysatomilky

FirebaseAuthentication と Firestore との戯れを頑張ってきたよなぁー。と

いま、こんな構成なんだけど、FirestoreとRepositories 部分が消える想定になるわけだ。
楽しみ。

satomilkysatomilky

Dailyの記録データはDailylogって名前にしてるけど、DailyDataって別途試す専用のモデルを作ってみるよ。

そして、ちょうど余分なTabが余ってたので、ここにリストを作ることにする。

今日は、一旦FirebaseAuthenticationとは別で、ログアウトしたとしても、SwiftDataで管理してるデータ自体は残っているかどうか

という確認までしたい。

satomilkysatomilky

ひょっとして、これ、MVVMの形式を取る必要がなくなってくるのでは。

satomilkysatomilky

どうせ、このView内でしか使わないし、分散してる方がわかりづらいから。。と思ったけど、

コレらのエラー見るとちゃんと別ファイルにした方がええんかなと思ってしまう。
まぁ、今のは暫定的に、今のFirestoreとの連結を殺さずに、追加実装として別複製ファイルを作った結果で、イレギュラーな事例ではあるけど。

satomilkysatomilky

カレンダーのリスト表示させて、タップすると


複製版 NewInputViewを読み込むところまではできた。

データ部分を置き換えするぞー。

けどViewModel側で連携してるデータモデルの部分の差し替えだけでいけるんだろうか。。わからん。

satomilkysatomilky

いや、さっきのなるほどじゃない。
結局、一件だけ取得するって方法じゃない。

@Attribute(.unique) みたいなことができるのに、
なんで、いわゆる findByみたいなやつができないの。

SwiftDataの実例からずっと探してるんだけど、
基本的に、配列データをForEachで回して、一件ずつデータを並べる。

みたいな例しかなくて、

今の私がやりたい 日付をユニークキーに、その日付のDataがあればそれを持ってくる。みたいなことをやってる事例が全然見当たらない。
なんでだろ。

satomilkysatomilky

これに近いな。
https://stackoverflow.com/questions/76445376/how-to-filter-object-from-array-using-swift-macros-in-swiftdata-and-swiftui

うちのアプリの場合は、時差考慮されると、色々計算がずれちゃうので、"2023-01-01" みたいな String化されたDateデータをキーに保存するんだけど。

ちょっとめんどくさすぎるな。もっとスマートに書けないのだろうか。

どうでもいいけど、なんで、.filer とか元々あるやつ使わなかったんだろ。。

satomilkysatomilky

Previewを使って。だけど、できたっぽい。

こんなふうに findByの拡張を作りました。ってかAppleから提供してくれよ。こんな基本的なやつ。。

extension Collection where Element == DailyData {

  func findBy(date: Date) -> DailyData? {

    let dateString = date.dateString!
    let predicate = #Predicate<DailyData> { $0.date == dateString }

    do {
      let result = try filter(predicate)
      return result.last ?? nil

    } catch let error {
        print("Failed to filter date record from DayLog. Error: \(error.localizedDescription)")
    }
    return nil
  }
}
satomilkysatomilky

できたー。一旦コアなDailylog(一時的にDailyRecordsという名前にしてるけど)の、値の取得から保存/updateの部分はできたぞ!!
それも現状のUIと変えずに。


そして、ViewModel使わなくなった。なんかすごい。
(私の必死に覚えたMVVMの勘所が全部無駄に。。。)

所要時間は10時間くらいか。
完全0知識から始めて調査も含めてだから、なかなか良い調子なのでは。

あと、SwiftData対応しなきゃいけないのCycleとSettingsだな。がんばろ。

Firestoreの時には、Dailylogの入力に合わせて、Cyclesデータの再計算(それも一部だけアップデートするっていう複雑なロジック組んでた)とかめっちゃ面倒なことやらなきゃいけなかったけど、
今回は全部ローカル上のデータだし、都度生成でいけるかな?

だとしたら、CycleはStoreするデータとしては持つ必要ないし、めっちゃ楽になる。
(あの複雑極まりなかったupdateロジックがいらなくなってしまうかもしれないの流石に切ない)

これを実装してた。。

とりあえずSettingsをやるか。

satomilkysatomilky

イニシャライザ必ず用意しなきゃ怒られるんだな。
初期値設定してるのに。

satomilkysatomilky

あれ?いや、SettingファイルなんだからNSUserDefaultを使えばいいのか?
SwiftData使う必要ない?

(でもNSUserDefaultってtypealiasしたモデルデータって入るっけ?あと、Doubleとか無理だった気がする。)

satomilkysatomilky

現行公開されてるのはNSUserDefaultだから、それそのまま使えばマイグレーションの必要もない!!
(ただ、そもそも前のバージョンがビルドできるのかわからん。)

satomilkysatomilky

@State変数の初期化はうまくいったけど、UserDefaultが標準で対応してない構造体を格納うまくできてなかったみたいで、詰んだ。

明日これためそう。。
https://capibara1969.com/2531/#toc7

と思ったけど、あれ?保存時にエラーってことは、rawValueに戻せば良いのでは?と、思い、以下で解決した。

satomilkysatomilky

終わったぁー。Setting系終わった。実際にコード触ってた時間は、やっぱり7時間くらいかな。。

列挙体をUserDefaultに突っ込む処理がなかなか文献なくて苦労した。
コレは今度、記事化しよ。

明日はCycleだー。(仕事があるけど。)

satomilkysatomilky

SwiftUIって、body内でView内で初期化するための引数の記述の問題に対して、エラーの出し方が間抜けすぎるだよな。

satomilkysatomilky

月火と本業と育児に追われて、ほとんど手をつけられてませんが、
いつも寝る前に30分だけ微妙に作業してたりします。

Firestoreで貯めてたデータ(コレクション)は

  • user(setting)
    • dailylogs
    • cycles

の3つでしたが、

userとdailylogsが終わりました。
残すところはcyclesだけです。。
だけど、dailylogsからcyclesを算出するところはすでにできていて、
cyclesの再計算をする際には、毎回dailylogs全件を遠慮なく持ってきてもいい。。
そして、昨日、dailylogsからcyclesを算出するところは、爆速で算出できるということが確認されたので。。。

なんというか、なんというか。。本当に、私、なぜ、Firestoreを使っていたのだろうか。と思っています。
ネットワーク性(複数ユーザーでドキュメントを参照しあうなど)が必要のないアプリで、特に個人ログ的なアプリで、Firestoreを使う必要はマジでまるでないですね。。。

というかSwiftDataが凄すぎるのかもしれん。

satomilkysatomilky

いかんいかん。うっかりAppleStoreのページを見入ってしまった。
毎年Mac買うとかあかんよ。

satomilkysatomilky

何日ぶりだろ。今日はCalendarのViewsから、ViewModel切り離し。

satomilkysatomilky

Cyclesは終わった。家事の合間にやってたから2時間くらい。
あとのViewsで、FirestoreとかFirebaseAuthenticationを取り除く作業だ。

satomilkysatomilky

SwiftDataへの移行(というか結局高速で計算するのでCycleは適時生成でSwiftDataで扱う必要がなくなった。)は終わって不要ファイルの整理してたんだけど、Modelを記載してるファイル構成とか整理してたら、
UnitTestが全然通らなくなった。

UnitTestを通したいのに、なぜかUITestの設定が干渉してくる感じがしたので、UITestそのものをTargetから削除したったわ。

satomilkysatomilky

SwiftDataに移行したタイミングのせいか、Xcode15のバグなのかわからないんですが、@Stateプロパティへの代入が一回めだけ上手く行かない。と言う状況が発生してる。

例えば、Cellを選択する @State selectedCell みたいなのがあるとして、タップしても一回目だけ、なぜか初期値のままだ。。

satomilkysatomilky

既存バグなのか、Xcode15からのバグなのかわからないけど、

この1行を加えて初めて、 @State var selectedDate の値 が .sheet 呼び出す時にも反映されるようになった。どう言うことなの。。

satomilkysatomilky

大体終わった。なんと。実質稼働時間でいったら調査も含めて、長く見積もっても、4人日くらいで終わってる気がする。

SwiftDataかなり良いかも。
(というかネットワーク接続前提のアプリの難易度がどんだけ高いのか。特に接続回数をできるだけ減らして低コストにするとかやり始めると、ほんとむずい。)

satomilkysatomilky

再オープンした。というのも、カレンダーViewがめっちゃ重いことに気づいたから、やはり、Dailylogだけでなくて、Dailylogから算出されるCycleもデータとしてきちんとストアされてた方が良さそう。という話からです。

すごくもたつくのですわ。。

https://youtube.com/shorts/ABqK67hXbfc?feature=share

satomilkysatomilky

そんなことなかった、上の7行目の記述、必要ないと思ったけど、必要だったらしい。

7行目に該当するものを作ったらいけた。

satomilkysatomilky

CycleListViewの方は、なぜか、こうやって一旦、格納してあげないとPredicateに使えなかった。。

多分、上のCalendadr Intervalがstructで、Cycleがclassだからかなと。。

冷静に考えれば、勘所としては構造体とクラスの違いでしょってわかるのに、昨日、この辺り作業した時に病み上がりな上に深夜だったのとで、さっぱり頭が回ってなかったです。

satomilkysatomilky

あとは、CycleをRenewするタイミングを見計らっていかねば。
は!これは、ここで頑張って作ったロジックが再び活かせるのでは!!

(SwiftData高速だし、読み込みと書き込みでお金取られないから、Renewすれば良いだけなんだけどな。)