- 投稿日:2021-06-21T23:57:02+09:00
自作パッケージをSwift Package Indexに追加する方法
「Swift Packagesによるライブラリの作成方法」は4部構成です。 記事を順番に読み進めると、Swift Packagesでライブラリを自作して公開できるようになります。 第1部: Swift Packagesでライブラリを自作する 第2部: 自作パッケージをSwift Package Indexに追加する ←イマココ 第3部: 自作パッケージをCocoaPodsに追加する 第4部: 自作パッケージをCarthageに対応する はじめに 自作パッケージをSwift Package Indexに追加する方法を紹介します。 「Swift Package Index」とは? 公式サイトのFAQを直訳すると「Swift Package Managerをサポートする、パッケージの検索エンジン」です。 パッケージを検索して、そのパッケージに関する様々な情報を得ることができるサイトです。 環境 OS: macOS Big Sur 11.4 Xcode:12.4 (12D4e) Swift:5.3.2 swift-tools:5.3 Swift Package Indexへの追加方法 Swift Package IndexへはPRを作成して手動で追加を依頼する必要があるので、その手順を紹介します。 パッケージを自作する Swift Package Indexへは他人が作成したパッケージを追加することもできるようですが、やはり自作パッケージがなければ始まりません。 以下の記事を参考にパッケージを自作します。 PackageListリポジトリをフォークする パッケージを自作したら、以下のリポジトリをフォークします。 私はフォーク元のリモートリポジトリを origin とし、フォークしたリモートリポジトリには作者(今回は自分)の名前を付けるのが好みなので、そのようにします。 $ git clone https://github.com/SwiftPackageIndex/PackageList.git $ cd PackageList $ git remote add uhooi https://github.com/uhooi/PackageList.git main ブランチから作業ブランチを切ります。 $ git switch -c feature/add_package JSONファイルに自作パッケージを追加する Package.json へ自作パッケージのURLを アルファベット順 に追加します。 Package.json [ ... "https://github.com/ucotta/brillianthtml5parser.git", + "https://github.com/uhooi/swift-string-transform.git", "https://github.com/uias/Pageboy.git", ... ] 追加したら swift ./validate.swift を実行し、JSONファイルに問題がないか確認します。 $ swift ./validate.swift Warning: Using anonymous authentication -- may run into rate limiting issues Processing package list ... + github.com/uhooi/swift-string-transform.git ✅ validation succeeded 追加したパッケージのURLと「✅ validation succeeded」が出力されたらバリデーション成功です。 PRを作成する バリデーションに成功したら、変更をコミットしてプッシュします。 $ git add Package.json $ git commit -m 'Add package' $ git push uhooi feature/add_package フォーク元のリモートリポジトリにPRを作成します。 タイトルや概要などは以下のPRを参考にしてください。 問題なければ数時間でメンバーの方がマージしてくださります。 Swift Package Indexでできること PRがマージされたらSwift Package Indexに自作パッケージのページができます Swift Package Indexでできることを抜粋して紹介します。 様々な環境でビルドしてバッジ化される 様々なプラットフォーム、Swiftのバージョン、ソース(最新タグとデフォルトブランチ)の組み合わせでビルドし、成否とログを確認できます。 CIが成功してからパッケージをリリースすることが多いとは思いますが、すべての組み合わせを網羅することはなかなかないので、非常にありがたいです。 ビルドに成功した環境がバッジ化されるので、READMEに付けるとどの環境で使えるかがすぐにわかります。 ちなみに新しいバージョンをリリースしたら自動で再ビルドしてくれます。 手動でリリースを伝える必要はありません。 同じ作者で他のパッケージを見れる Xcode 13で追加された「Package Collections」という仕組みがあり、複数のパッケージをJSONファイルにまとめることでパッケージを探しやすくなります。 Swift Package Indexでは同じ作者のパッケージをまとめたコレクションのJSONファイルを自動で作成してくれます。 その場でパッケージを試せる 「Try in a Playground」をクリックすることで、パッケージがセットアップされた状態でPlaygroundが起動し、すぐにパッケージを試すことができます。 spi-playgrounds://open?dependencies=uhooi/swift-string-transform 事前にSwift Package Index Playgrounds appをインストールする必要があります。 おわりに 自作パッケージをSwift Package Indexに追加することができました! 自分のパッケージが素晴らしいサイトに載るのはとても嬉しいです Swift Package Indexは非常に便利なサイトなので、パッケージを自作していない人もぜひ活用してみてください! 参考リンク FAQ – Swift Package Index Add a Package – Swift Package Index https://twitter.com/the_uhooi/status/1404723147509690379?s=20 https://twitter.com/the_uhooi/status/1404784236050096129?s=20
- 投稿日:2021-06-21T20:18:31+09:00
Index out of rangeをこの世から無くす方法
はじめに こんにちは@kaneko77です。 大それた記事のタイトルに今回はなりましたが、 配列の数よりも大きい数を指定すると出るあいつをもう駆除しちゃおうぜって話になります。 今回はIndex out of rangeのエラーをもう見なくて良い方法を共有していきたいと思います。 マエフリ Index out of range ってなに? 念の為の説明になります。 なんの工夫も無いただの配列があります。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] 配列の中身は見ての通りですが4つになります。 print(test.count) // 4 以下をやってしまうと当然...エラーになります。 要素数以上に指定してしまってるからですね。 これがIndex out of rangeが起きる原因です。 配列はこの場合だと要素は0番目から3番目までです。 print(test[4]) <以下エラー画面> 原因について 先ほど説明したように いうまでもなく配列の要素数より上の数を指定するから「そんな要素ないわ」って 怒られますよね。 これがシンプルな原因ですが、しかし頭でわかっていてもやっぱりミスってあるし プログラム的に要素の指定するってなると 余計気を配らなければいけないですよね。 解決策 ということでやっと紹介できます。 定義した要素を超えた数をどうにかしなければいけないです。 方法として何点かありますが、手っ取り早いのが要素より超えた数を指定した場合nilを 返す方法が一番楽だと思います。 コード nilを吐き出してくれるチェック用のextension作成 まず以下のコードが必要になります。 以下のコードはこちらを参考にしました。 extension Array { subscript (element index: Index) -> Element? { // MARK: 配列の要素以上を指定していたらnilを返すようにする indices.contains(index) ? self[index] : nil } } 呼び出し側 以下でnilが返すようになりました。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] print(test[element: 4]) // nil 上記だとnilの制御しなければいけないので以下のようにしました。 こんな感じです。 これでnilチェックが入ったコードになりました。 let test = ["マチュピチュ", "モンサンミッシェル", "サグラダファミリア", "アンコールワット"] guard let list = test[element: 4] else { print("nilだったみたいですよ。") return } // 現状はここに入ります print("受け取れたみたいです。", list) 終わりに 今回は大それたタイトルでご紹介しましたが, エラー潰しのストレスを少しは消せる技術ではないかと思います。 ここ前で見ていただきありがとうございます。
- 投稿日:2021-06-21T11:13:50+09:00
【社内勉強会資料】iOSアプリ開発のナレッジ全般�〜管理・設計・テスト・リリース・運用のポイント〜
この記事は何 先日行った社内勉強会のスライドを元に書き起こしたものです。 iOSアプリ開発のマネジメント、設計、テスト、リリース後の運用(維持保守)において考慮すべきポイントについて解説しました。 開発者だけでなく、マネージャやソリューションアーキテクトも意識した内容になっていますので、実装に関する細かい話は出てきません。 認識違いなどがありましたら、コメント欄で教えていただけるとありがたいです。 XcodeとiOSのバージョン追従について Xcodeの概要 Xcodeは、Apple純正で、Appleプラットフォーム※のアプリを開発するための統合開発環境。(※今回はiOSのみに話を絞る) XcodeはMacにしか入れられないのでMacは必須。 iOS SDKが内包されており、XcodeのバージョンとiOS SDKのバージョンは対応している(後述)。 iOSシミュレータが内包されており、実機がなくてもある程度のデバッグは可能。ただし加速度センサー、カメラ、マイクなど、一部のセンサー/デバイスを使うテストはできない。 SwiftとObjective-Cコンパイラが内包されており選択可能だが、今やSwiftの方がネット上にサンプルコード等の情報が圧倒的に多く、Null安全性などのメリットも有り、新規アプリであればSwift一択。 Xcodeのバージョンアップ追従 iOSがバージョンアップされると、そのSDKを含むXcodeがリリースされる。→Xcodeを上げないと新機能のAPIが使えない Xcodeのメジャーアップデートが毎年9月にあり、翌年4月以降、最新メジャーVerのXcodeでビルドしないとビルドをAppleのサーバーにアップロードできないことが慣例化。 Xcodeのバージョンアップに伴い、Swiftコンパイラのバージョンが上がって、アプリのコードを修正しなければならないことがある。また、ライブラリの対応状況の事前調査が必要。 iOSの新バージョンへの追従 似たような話題として、リリース済みアプリが、新OSバージョン下ではユーザーが期待しない挙動となってしまう場合がある。 iOS 14リリース時の事例: プライバシー保護のため、アプリがコピペ内容を読み取るとバナーで通知される機能が追加された。この影響を受けてしまい、一部のライブラリのアップデートが必要となった。 デフォルトのブラウザをユーザーが設定できる機能が追加された。 Safari以外に設定された場合に、アプリからブラウザが開けなくなってしまい、アプリ改修が必要となった。 アプリ運用保守の年間スケジュール例 開発ライセンスについて Apple Developer Program (ADP) アプリをApp Storeで配布するための開発ライセンス。 App審査あり。 個人、法人ともにライセンス取得可能。 -法人の場合、実在を証明する「D-U-N-S Number」の取得が必要。(東京商工リサーチに申請) 法人の場合、アプリを「所有する」法人自身がライセンスを取得する必要あり。受託業者は開発者としてチームに招待される形。 99 US$/年 内部テストのためのアプリ配布については後述 Apple Developer Enterprise Program (ADEP) 社内用アプリを配布するための開発ライセンス。 逆にApp Storeでの配布はできない。(配布用サーバーが必要) App審査なし。 従業員100名以上であることが条件。 「D-U-N-S Number」の取得が必要。 社外にアプリを配布することは規約違反となる。 現状、ADEPを取得するのは非常に難しい模様。この規約を守らない事例が多いため? 399 US$/年 Human Interface Guideline (HIG) Apple公式のUI/UXデザインガイド。 デザイナー向けのガイドではあるが、HIG違反はApp審査リジェクトの可能性があるため、実質「開発ルール」でもある。 このため、iOS Developerは(できればManagerやArchitectも)HIGを十分理解する必要があり、また頻繁にアップデートされるため最新情報のキャッチアップが必要。 ライブラリについて よほど機能がシンプルなアプリでない限り、OSSライブラリの利用は避けられない。一から全て自作するには工数も技術力も必要なので… FirebaseなどmBaaSのSDK(※Firebaseの紹介は後述) 実装を簡易にするためのライブラリ アニメーション、ネットワーキング、DB、Keychainアクセスなど ただし、Xcodeの項で触れた通り、ライブラリ起因で「Xcodeのバージョンアップができない」等の運用トラブルが起こりがちなので、利用ライブラリの種類とバージョンの管理が重要。 ライブラリ管理ツール CocoaPods 最も歴史が長く、対応しているライブラリが多いため無難な選択肢。 Xcodeのプロジェクト定義ファイル(XML)を直接自動編集してしまう仕様のためトラブルが発生すると解決が大変。 Carthage(カルタゴ/カーセッジ) ビルドが高速なことが売り。 対応していないライブラリもちらほらある。 Swift Package Manager Xcode 9で同梱された比較的新しめの公式ツール。 徐々に対応しているライブラリが増えてきたので新規アプリの選択肢としては大いにアリ。 Firebaseについて モバイルアプリの開発や運用のための便利なクラウドサービス。 無料(2021年6月現在)で使える鉄板サービス Analytics: アプリでのユーザー行動をデータ化し分析できる機能。 Cloud Messaging (FCM): Apple Push Notificationサービス(APNs)との中継を行なってくれるサービス(詳細は後述)。ユーザー属性と行動に合わせて作成したセグメントに対して、メッセージを送信するような使い方が可能。 Crashlytics: クラッシュレポーティング。Xcodeの標準機能よりも反映が早くて見やすい。 認証サービスやリアルタイムDBなどの便利なサービスもあるが、有料である プッシュ通知 全体的な仕組みおよびFCMを使うメリット FCMを使わない場合のバックエンドの煩雑さ ・iOSはAPNs、AndroidはFCMにそれぞれ異なるAPIでリクエストを送らなければならない。 ・バックエンドサーバーにAPNs用の証明書または認証キーを登録しなければならない。 ↓ FCMを使うことで解消される点 ・iOS、Androidとも同じAPIでリクエストを送れる。 ・APNs用の証明書または認証キーはFCMの管理コンソールで登録すれば良い。 最も基本的なペイロード 規定項目はiOSが勝手に処理するためアプリが取得する必要はない。 それ以外にアプリとして自由にカスタム項目を追加できる。 それを利用して、「この値のプッシュ通知を受け取ったら画面Xに遷移する」などの機能を実現できる。 リッチな通知 iOS 10以降では、画像・動画・音声、カスタムアクションが使える。 iOS 12以降の機能 通知センター上でのグルーピングが可能となった。 ユーザー許諾を初回の通知受信時に行うことが可能になった。 サイレントプッシュ通知 特徴 画面上には何も表示されない通知。 iOSは通知を受け取ると該当アプリをバックグラウンドで起動する。 アプリはこれを受けたタイミングで何かしらのバックグラウンド処理を起動することができる(サーバーから最新データを取得する等)。 通知を受けた際のフック処理 「何かしらの処理」は最大30秒間しか実行できない。 実行タイミングはiOSが判断するため(充電状態や通信状態等?) 、全く実行されない場合や、遅延される可能性がある。 バックグラウンド処理 全体感 iOSではアプリのバックグラウンド処理について制約が大きい。 実行タイミングがシステムに依存している。 処理時間が制限されている。 実行時点での環境(バッテリー状況、通信状況など)によって実行されない場合もある。 ユーザーが「設定」appにてオプトアウトできる。 定型的なバックグラウンド処理 オーディオ再生、AirPlay、Picture in Picture 通話、通話用のプッシュ通知 サイレントプッシュ通知(※) 位置情報の取得(※) 外部アクセサリやBluetooth機器との通信(※) (※)ユーザーの許諾が必要なもの 任意のバックグラウンド処理 iOS 12以前 Background Task Completion フォアグラウンドでやり残した作業をバックグラウンド状態でも引き継いで実行させることが可能。 実行可能時間は不明(最大30秒?)。 Background Fetch SNSアプリのタイムライン事前取得等を想定した機能。 ユーザによるオプトアウトが可能。「設定」app>対象アプリ>「Appのバックグラウンド更新」 OSによってスケジューリングされ、遅延されたり実行されない場合も。 端末や該当アプリの利用傾向をOSが自動判定してタイミングを決めているらしい。 実行可能時間は30秒。 iOS 13以降 iOS 13で“BackgroundTasks Framework”が登場。 Background App Refresh Tasks Background Fetchの後継であり、基本は変わらず。 Background Processing Tasks iOS 13から利用可能な新機能 比較的重たい処理を想定し、数分間の実行時間が許されている。 例えば、Core MLを利用した機械学習のトレーニング処理などを想定。 アプリ側で条件設定が可能(Wi-Fi接続時とか、充電中とか) ただし、条件を満たしても確実に実行される保証はない。 ローカルデータストア Key-Value Store UserDefaults アプリ内部に保存される アプリを削除すると値が消失する 平文で保存される →センシティブ情報の保存には向かない Keychain デバイスのiCloud Keychainに保存される アプリを削除しても値は無くならない(再インストールしたら復活する) 暗号化され保存される+プロビジョニング・プロファイル(※)によって読み出し保護されている。 (※)開発者証明書とアプリIDによって正規のアプリであることを担保するためのプロファイル Database CoreData iOS SDK同梱 SQLiteのwrapper 暗号化したければOSSライブラリが必要。 Realm(レルム) →個人的におすすめ 鉄板のOSS オブジェクトデータベースで、エンティティをSwiftの型で定義できる。 永続化するかオンメモリーだけで保持するかを簡単に切り替えられる。 暗号化機能がある。 Webブラウジング SFSafariViewController ブラウザアプリに飛ばすよりもアプリと一体感がある。 見た目を変える、ボタンを追加する等のカスタマイズはほとんどできない。 イベントのフックなどもほとんどできない。 アプリからSFSafariViewControllerに認証情報は渡せない(IFがない)。つまり、アプリ側でサービスにログインしても、Web側のコンテンツを開くときに再ログインが必要になるので設計上の工夫が必要。 ドメイン名が表示されるのでオープンリダイレクト対策効果がある。 WKWebView 見た目を変える、ボタンを追加する等のカスタマイズが可能。逆にデフォルトはノッペラボーなので、閉じるボタン、ローディングインジケーター等、全て実装が必要。 URLやドメイン名は見えない。必要なら実装が必要。 Viewの一部に埋め込むこともできる。 イベントのフックが可能(リクエスト時、ロード開始時、ロード終了時などなど)。 Swiftコードで要素を拾うことやJSを注入することも可能。 認証Cookieを渡すことも可能。 ただし、iOSに内包されているWebKitの影響を受けて、iOSのバージョンアップ後に以前とは違った挙動をしたりする等のトラブルがある。 ご利用は計画的に。 テスト TestFlight ADPライセンスを取得すると、“App Store Connect”に、アプリの公開のための情報を登録することができるようになる。 App Store Connectにはテストのための“TestFlight”機能がある。 テスターのApple IDを登録できる。 開発者がアプリのビルドをアップロードすると、テスターのiOSデバイスのTestFlightアプリからテストアプリをダウンロード可能になる。 Jenkins, Bitrise, CircleCIなどを利用してCI/CDも可能。 テスターの種類 内部テスター App Store ConnectのロールはAccount Holder(アカウントホルダー)、Admin(管理者)、App Manager(App管理者)、Developer(デベロッパ)、Marketing(マーケティング)で、最大100人まで。 内部テスターは、上記のメンバーを設定できる。 各自最大30台のデバイスをテスト用デバイスとして登録できる。 外部テスター 最大10,000人まで、Eメールやリンクによって招待できる。 未リリースのアプリは、簡易的な審査を受けなければ、外部テストはできない。 テスト用ビルドは90日で無効になり、起動できなくなる。 テストのTips 本番用のアプリとはBundle ID(アプリID)を変えて、テスト用のアプリを別に作り、開発中はそちらを使ってテストした方が良い。 プッシュ通知など、間違えて本番向きに飛ばしてしまうと事故になってしまう。 クラッシュレポートに開発中/本番のログが混在してしまう。 テスト用アプリのみ、サーバーの接続先変更機能を持たせる。 リリース直前の最終検証フェーズから本番アプリに切り替える、などの運用が良い。 ただし、本番/テストビルドの切り替えを自動化するために、設定ファイルによるDI、ビルドスクリプト、あるいはブランチ運用などの手法の検討が必要。 自動テスト XCTest ロジックのユニットテスト。 Swiftでテストコードを記述する。要はxUnit。 XCUITest 自動シナリオテスト。Swiftで「実行ボタンをtapする」「アラートに◯◯という文字列が表示されること」というようなテストコードを書くと、シミュレータ上で実際に動作してテストが行われる。 テストコードからテストシナリオを読み取ることが難しく、メンテナンスがおざなりになってテストが陳腐化しがち。 実行時間が長い。テストシナリオが100を超えると1〜2時間以上は掛かる。 マネジメント層は自動テストに夢を抱きがちだが、現実はそんなに簡単ではない。 自動テストの導入には、戦略、計画、合意が重要。 App審査提出 大まかな流れ App Store Connectにアプリの基本情報(アプリID、説明文、検索キーワード、個人情報収集の説明etc)を登録する。 App Store上に表示させるアイコン画像を作成し登録する。 同、スクリーンショット画像(または動画)を作成し登録する。 プライバシーポリシー用Webページを作成し、リンクを登録する。 サポート用Webページを作成し、リンクを登録する。 アプリのビルドをアップロードしビルド番号を指定してsubmit。 主にプロダクトオーナーとデザイナーに負荷が掛かるフェーズ。 App審査の注意点 App Store Reviewガイドラインは一度目を通したほうが良い。 審査は結局レビューアの主観なので、今まで指摘されなかった問題を急に指摘されリジェクトを受けることもある。 審査にかかる日数は以前と比較すると短縮される傾向がある。 2020年から、バグ修正の場合は法的問題以外はガイドライン違反があってもリジェクトされず公開できることになった。 リリース計画には、審査リジェクトのリスクを考慮する必要あり。 実際に遭遇したリジェクト事例 非公開APIを使用している。 使用禁止となった旧APIを使用している。 “Android”のような、他のモバイルプラットフォームに関連する単語を表示している。 「ボタンtapでSafariに飛ばす」は“Minimum Functionality”なのでSFSafariViewControllerを使え。 App Store Connect上で入力したバージョンアップ説明文にて未発表のApple製品名に言及している。 機能を見られずレビューできないので動画を添付せよ。 おわりに iOSアプリ開発に携わる方にとって大事なこと Appleのポリシーを知ること。 Appleの最新動向をキャッチアップすること。 問題に突き当たったときは、まず公式ドキュメントを調べること。 公式ではないWeb記事等を参考にするときは、記事の執筆日時をチェックするクセをつけること(3年以上前の記事は当てにならないことが多い)。
- 投稿日:2021-06-21T09:45:15+09:00
iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編)
iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編) ← いまここ 前置き 前回、前々回とモバイルアプリにおける状態管理の一つの考え方と、コードに落とし込むための実装方針、そしてなぜこのライブラリを作成したのかについてお話してきました。 今回は純粋に作成したStoreFlowableライブラリでどういった機能を提供しているかと、その使い方について紹介したいと思います。 Dart版も同等の機能を提供していますが、まだドキュメントやサンプルコード、テストなどが準備できていないので本記事ではKotlin版とSwift版を元に紹介します。 サンプルコード それぞれのリポジトリにはサンプルコードが含まれています。Kotlin版 / Swift版 下記の使い方と合わせて見ていただくと良いかと思います。 導入 Kotlin版はMavenCentralで配信しているので、Gradleの依存に以下を追加して下さい。 Kotlin dependencies { implementation("com.kazakago.storeflowable:storeflowable:x.x.x") } Swift版はSwift Package Managerでの導入が可能です。 Swift dependencies: [ .package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "x.x.x"), ], 基本的な使い方 1. FlowableDataStateManagerを継承したシングルトンクラスを作成する まず、FlowableDataStateManager<KEY>継承クラスをシングルトンクラスとして作成して下さい。 このクラスが前回の記事でも解説している、状態を通知できる仕組みを内包しています。 Kotlin object UserStateManager : FlowableDataStateManager<UserId>() Swift class UserStateManager: FlowableDataStateManager<UserId> { static let shared = UserStateManager() private override init() {} } 2. StoreFlowableFactory<KEY, DATA>を実装したクラスを作る まず、リモートからデータを取得するApiクラスとローカルキャッシュへのデータ入出力Cacheクラスを用意してください。 ここでは便宜上UserApiクラスとUserCacheクラスとします。 次にStoreFlowableFactory<KEY, DATA>の実装クラスを作ります。 このクラスがデータごとに処理が変わる、共通化部分としてまとめることが出来ない部分を記述したクラスとなります。 以下に例を示します。 Kotlin // StoreFlowableFactoryを実装したクラスを作成してください。 // 同一のデータが複数存在する場合はジェネリクスのKEYにその区別となる型を指定して下さい。データが一つの場合はUnitでOKです。 // DATAジェネリクスには扱うデータの型を指定して下さい。 class UserFlowableFactory(userId: UserId) : StoreFlowableFactory<UserId, UserData> { private val userApi = UserApi() private val userCache = UserCache() // データが複数存在する場合はその区別となるデータを渡して下さい。 override val key: UserId = userId // 作成したFlowableStateManagerのシングルトンインスタンスを指定して下さい。 override val flowableDataStateManager: FlowableDataStateManager<UserId> = UserStateManager // ローカルキャッシュからの取得処理を実装して下さい override suspend fun loadDataFromCache(): UserData? { return userCache.load(key) } // ローカルキャッシュへの保存処理を実装して下さい override suspend fun saveDataToCache(data: UserData?) { userCache.save(key, data) } // リモートからの取得処理を実装して下さい override suspend fun fetchDataFromOrigin(): FetchingResult<UserData> { val data = userApi.fetch(key) return FetchingResult(data = data) } // キャッシュが有効かどうかを判断する処理を実装して下さい。キャッシュが期限切れする必要がなければ常にfalseを返してしまってもOKです。 override suspend fun needRefresh(cachedData: UserData): Boolean { return cachedData.isExpired() } } Swift // StoreFlowableFactoryを実装したクラスを作成してください。 // 同一のデータが複数存在する場合はassosiatedTypeのKEYにその区別となる型を指定して下さい。データが一つの場合はUnitHashを指定して下さい。 // DATA assosiatedTypeには扱うデータの型を指定して下さい。 struct UserFlowableFactory : StoreFlowableFactory { typealias KEY = UserId typealias DATA = UserData private let userApi = UserApi() private let userCache = UserCache() init(userId: UserId) { key = userId } // データが複数存在する場合はその区別となるデータを渡して下さい。 let key: UserId // 作成したFlowableStateManagerのシングルトンインスタンスを指定して下さい。 let flowableDataStateManager: FlowableDataStateManager<UserId> = UserStateManager.shared // ローカルキャッシュからの取得処理を実装して下さい func loadDataFromCache() -> AnyPublisher<UserData?, Never> { userCache.load(userId: key) } // ローカルキャッシュへの保存処理を実装して下さい func saveDataToCache(newData: UserData?) -> AnyPublisher<Void, Never> { userCache.save(userId: key, data: newData) } // リモートからの取得処理を実装して下さい func fetchDataFromOrigin() -> AnyPublisher<UserData, Error> { userApi.fetch(userId: key).map { data in FetchingResult(data: data) }.eraseToAnyPublisher() } // キャッシュが有効かどうかを判断する処理を実装して下さい。キャッシュが期限切れする必要がなければ常にfalseを返してしまってもOKです。 func needRefresh(cachedData: UserData) -> AnyPublisher<Bool, Never> { cachedData.isExpired() } } <KEY>の利用するシーンとしては、例えばGET /users/{user_id}/reposのようなREST APIがある場合などに、UserIdごとにキャッシュを保持しておきたいケースなどに使用して下さい。 このような場合分けが不要な場合は<KEY>にKotlin版ではUnitを指定して下さい。SwiftではUnitHashというstructを作成してあるのでそちらを指定して下さい。 3. Repositoryクラスを作成する ここまででStoreFlowableを利用するための準備は整っているので、Repositoryパターンを体現したクラスを作成します。 2.で作成したクラスのインスタンスに生えているcreate()メソッドからStoreFlowableクラスを作成できます。 このクラスが本ライブラリの本体となり、データの監視や入出力を司るメソッドが生えています。 Kotlin class UserRepository { fun followUserData(userId: UserId): Flow<State<UserData>> { val userFlowable: StoreFlowable<UserId, UserData> = UserFlowableFactory(userId).create() return userFlowable.publish() } suspend fun updateUserData(userData: UserData) { val userFlowable: StoreFlowable<UserId, UserData> = UserFlowableFactory(userData.userId).create() userFlowable.update(userData) } } Swift struct UserRepository { func followUserData(userId: UserId) -> AnyPublisher<State<UserData>, Never> { let userFlowable: AnyStoreFlowable<UserId, UserData> = UserFlowableFactory(userId: userId).create() return userFlowable.publish() } func updateUserData(userData: UserData) -> AnyPublisher<Void, Never> { let userFlowable: AnyStoreFlowable<UserId, UserData> = UserFlowableFactory(userId: userId).create() return userFlowable.update(newData: userData) } } データの入出力を行う際には必ずこのStoreFlowableを経由して行って下さい。 データの実体を直接書き換えてしまうと変更通知が行われません。 データの監視を行いたい場合はpublish()を用いて下さい。 このメソッドを通すことで考え方編で当初目指していたRepositoryクラスのインターフェースを提供することが出来ます。 4. 作成したRepositoryクラスを利用する 作成したリポジトリクラスを利用するには利用側(ActivityやViewController、ViewModelなど)で監視するメソッドを実行します。 Kotlin版であればFlowによる通知なのでcollect {}, Swift版であればCombineによる通知なので sink {}を利用してデータ監視を開始できます。 また、返却されるStateクラスやStateContentクラスはdoActionメソッドで状態の分岐が可能です。 データの状態(停止状態・取得中状態・エラー状態)とデータの有無(存在する・存在しない)の組み合わせの最大6パターンの分岐を表示上網羅することができれば、データが将来的にいかなる状態になってもカバーできます。 Kotlin private fun subscribe(userId: UserId) = viewModelScope.launch { userRepository.followUserData(userId).collect { it.doAction( onFixed = { ... // 停止状態 }, onLoading = { ... // 取得中状態 }, onError = { exception -> ... // エラー状態 } ) it.content.doAction( onExist = { userData -> ... // データが存在する }, onNotExist = { ... // データが存在しない } ) } } Swift private func subscribe(userId: UserId) { userRepository.followUserData(userId: userId) .receive(on: DispatchQueue.main) .sink { state in state.doAction( onFixed: { ... // 停止状態 }, onLoading: { ... // 取得中状態 }, onError: { error in ... // エラー状態 } ) state.content.doAction( onExist: { userData in ... // データが存在する }, onNotExist: { ... // データが存在しない } ) } .store(in: &cancellableSet) } ここまでが基本的な使い方となります。 適切に使えば表示の不整合を解消しつつ、リモートとキャッシュの抽象化が実現できているはずです。 その他の機能 State<T>が不要な一度きりのデータ取得を行いたい場合 ここまでデータを監視して変化を受け取れることを前提にお話してきましたが、常にデータ監視が適切とは限りません。 その瞬間のデータが一度だけ必要で継続的な監視を必要としない場合はgetData()あるいはrequiredData()メソッドを使って下さい。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun getData(from: GettingFrom = GettingFrom.Both): DATA? suspend fun requireData(from: GettingFrom = GettingFrom.Both): DATA } Swift public extension StoreFlowable { func getData(from: GettingFrom = .both) -> AnyPublisher<DATA?, Never> func requireData(from: GettingFrom = .both) -> AnyPublisher<DATA, Error> } requiredData()は有効なキャッシュが存在せず、リモートからのデータ取得にも失敗した場合は例外を投げます。 getData()では例外の代わりにnull, nilを返します。 引数のGettingFromはどこからデータを取得するかを指定します。 デフォルトは両方からよしなに取得する.Bothですが、キャッシュからのみ取得する.Cacheと、リモートからのみ取得する.Originを指定することも出来ます。 enum class GettingFrom { Both, Origin, Cache, } 画面上でデータの取得から表示を行う場合は基本的にはpublish()によるデータ監視の仕組みを利用して下さい。 requiredData(), getData()についてはデータ監視と相性の悪い場合のみ使用して下さい。 データを強制的に更新する 通常の使い方ではキャッシュが無効にならない限りはリモートから新しいデータは取得しませんが、要件によっては監視を開始するタイミング(画面を開いたときなど)でデータの更新を強制的に行いたい場面もあると思います。 その場合はpublish()メソッドのforceRefresh引数にtrueを指定して下さい。 Kotlin interface StoreFlowable<KEY, DATA> { fun publish(forceRefresh: Boolean = false): Flow<State<<DATA>> } Swift public extension StoreFlowable { func publish(forceRefresh: Bool = false) -> AnyPublisher<State<DATA>, Never> } また、監視開始のタイミングではなく任意のタイミングでデータの更新をしたい場合はrefresh()を用いることも可能です。 引っ張って更新の機能などを提供する場合に利用して下さい。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun refresh(clearCacheWhenFetchFails: Boolean = true, continueWhenError: Boolean = true) } Swift public extension StoreFlowable { func refresh(clearCacheWhenFetchFails: Bool = true, continueWhenError: Bool = true) -> AnyPublisher<Void, Never> } clearCacheWhenFetchFailsはリモートからのデータ取得失敗時にローカルキャッシュを消去するか、continueWhenErrorはすでにエラー状態のときにデータ取得を継続するかどうかを変更できます。 いずれもtrueがデフォルトの振る舞いとなります。 キャッシュデータが有効か検証する 現時点で保持するデータが有効か検証したい場合はvalidate()が利用できます。 キャッシュが無効であればリモートからの再取得処理が実行されます。キャッシュが有効であれば何もしません。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun validate() } Swift public protocol StoreFlowable { func validate() -> AnyPublisher<Void, Never> } キャッシュデータを更新する なんらかの処理の都合でキャッシュデータを更新する必要がある場合はupdate()メソッドが使用できます。 null, nilを指定することでキャッシュデータの削除も可能です。 このメソッドからキャッシュを更新することで、データの変更通知が発火しすべてのデータ監視者にデータの変更が反映されます。 Kotlin interface StoreFlowable<KEY, DATA> { suspend fun update(newData: DATA?) } Swift public protocol StoreFlowable { func update(newData: DATA?) -> AnyPublisher<Void, Never> } State<T> 関連オペレーター State<T>に内包されるデータをストリーム内で触りたい場合に便利なオペレータをいくつか用意しています。 Flow<State<T>>、AnyPublisher<State<T>, Never>の変換 Flow<State<T>>、AnyPublisher<State<T>, Never>のストリーム内でデータを別のデータに置き換えたい場合にはmapContent()という関数を利用できます。 Kotlin val flowState: Flow<State<Int>> = ... val flowMergedState: Flow<State<String>> = flowState1.mapContent { value -> value.toString() } Swift let statePublisher: AnyPublisher<State<Int>, Never> = .. let mergedStatePublisher: AnyPublisher<State<String>, Never> = statePublisher1.mapContent { value in String(value) } Flow<State<T>>、AnyPublisher<State<T>, Never>の統合 複数のFlow<State<T>>>、AnyPublisher<State<Int>, Never>を統合したい場合は、Kotlinの場合はcombineState()、Swiftの場合はzipState()が利用できます。 Kotlin val flowState1: Flow<State<Int>> = ... val flowState2: Flow<State<Int>> = ... val flowMergedState: Flow<State<Int>> = flowState1.combineState(flowState2) { value1, value2 -> value1 + value2 } Swift let statePublisher1: AnyPublisher<State<Int>, Never> = .. let statePublisher2: AnyPublisher<State<Int>, Never> = .. let mergedStatePublisher: AnyPublisher<State<Int>, Never> = statePublisher1.zipState(statePublisher2) { value1, value2 in value1 + value2 } 片方のステータスがLoadingやErrorだった場合は全体がLoadingやErrorとして扱われるのでご注意下さい。 状態の優先度は Error > Loading > Fixed となります。 ページネーションサポート APIから取得したリモートのデータとローカルキャッシュのデータをうまくやりくりしないといけない一般的なユースケースの一つとしてページネーションがあります。 下記のようにリストの最下部に達したときに追加をAPIから読み込んで繋ぎ合わせるような仕組みです。 これに関してもRepositoryよりも外側でキャッシュを意識せずに利用するための追加クラスを提供しています。 1. PaginatingStoreFlowableFactoryを実装する この機能を使うにはStoreFlowableFactoryの代わりにPaginatingStoreFlowableFactoryを実装したクラスを作成して下さい。 基本的には同じですがsaveAdditionalDataToCache()とfetchAdditionalDataFromOrigin()を追加で実装する必要があるという部分が違いとしてあります。以下に実装例を示します。 Kotlin class UserListFlowableFactory : PaginatingStoreFlowableFactory<Unit, List<UserData>> { private val userListApi = UserListApi() private val userListCache = UserListCache() override val key: Unit = Unit override val flowableDataStateManager: FlowableDataStateManager<Unit> = UserListStateManager override suspend fun loadDataFromCache(): List<UserData>? { return userListCache.load() } override suspend fun saveDataToCache(newData: List<UserData>?) { userListCache.save(newData) } // 追加読み込みを行ったデータをキャッシュデータとどう繋ぎ合わせるかを定義して下さい override suspend fun saveAdditionalDataToCache(cachedData: List<UserData>?, newData: List<UserData>) { val mergedData = (cachedData ?: emptyList()) + newData userListCache.save(mergedData) } override suspend fun fetchDataFromOrigin(): FetchingResult<List<UserData>> { val fetchedData = userListApi.fetch(1) return FetchingResult(data = fetchedData, noMoreAdditionalData = fetchedData.isEmpty()) } // 追加読み込みを行う際のリモートからのデータの取得処理を記載して下さい。 // これ以上追加のデータが存在しないことがわかっている場合は戻り値のnoMoreAdditionalDataにtrueを指定して下さい override suspend fun fetchAdditionalDataFromOrigin(cachedData: List<GithubOrg>?): FetchingResult<List<GithubOrg>> { val page = (cachedData?.size ?: 0) / 10 + 1 val fetchedData = userListApi.fetch(page) return FetchingResult(data = fetchedData, noMoreAdditionalData = fetchedData.isEmpty()) } override suspend fun needRefresh(cachedData: List<UserData>): Boolean { return cachedData.last().isExpired() } } Swift struct UserListFlowableFactory : PaginatingStoreFlowableFactory { typealias KEY = UnitHash typealias DATA = [UserData] private let userListApi = UserListApi() private let userListCache = UserListCache() let key: UnitHash = UnitHash() let flowableDataStateManager: FlowableDataStateManager<UnitHash> = UserListStateManager.shared func loadDataFromCache() -> AnyPublisher<[UserData]?, Never> { userListCache.load() } func saveDataToCache(newData: [UserData]?) -> AnyPublisher<Void, Never> { userListCache.save(data: newData) } // 追加読み込みを行ったデータをキャッシュデータとどう繋ぎ合わせるかを定義して下さい // これ以上追加のデータが存在しないことがわかっている場合は戻り値のnoMoreAdditionalDataにtrueを指定して下さい func saveAdditionalDataToCache(cachedData: [UserData]?, newData: [UserData]) -> AnyPublisher<Void, Never> { let mergedData = (cachedData ?? []) + newData return userListCache.save(data: mergedData).map { data in FetchingResult(data: data, noMoreAdditionalData: data.isEmpty) }.eraseToAnyPublisher() } func fetchDataFromOrigin() -> AnyPublisher<FetchingResult<[UserData]>, Error> { userListApi.fetch(page: 1) } // 追加読み込みを行う際のリモートからのデータの取得処理を記載して下さい。 func fetchAdditionalDataFromOrigin(cachedData: [UserData]?) -> AnyPublisher<FetchingResult<[UserData]>, Error> { let page = ((cachedData?.count ?? 0) / 10 + 1) return userListApi.fetch(page: page).map { data in FetchingResult(data: data, noMoreAdditionalData: data.isEmpty) }.eraseToAnyPublisher() } func needRefresh(cachedData: [UserData]) -> AnyPublisher<Bool, Never> { cachedData.last.isExpired() } } このFactoryクラスから、create()で本体となるPaginatingStoreFlowableクラスを作成することができます。 2. requestAdditionalData()で追加読み込みを行う PaginatingStoreFlowableも通常のStoreFlowableクラスと使い方にほとんど変わりはありませんが、追加読み込みを行うためのメソッドが追加されています。 Kotlin interface PaginatingStoreFlowable<KEY, DATA> { suspend fun requestAdditionalData(continueWhenError: Boolean = true) } Swift public extension PaginatingStoreFlowable { func requestAdditionalData(continueWhenError: Bool = true) -> AnyPublisher<Void, Never> } このメソッドを呼ぶことで自動的にデータがつなぎ合わされた状態で通知されます。 すでに読込中の状態で連続でこのメソッドが呼ばれても、前回解説したとおり多重にAPIがリクエストされてしまうことはありません。 ゆえに画面側で読込中かどうかを判定して、メソッドを呼ぶかどうかを分岐させる処理は不要です。 繋ぎ合わされたデータは毎回まとめて通知されるため、実際に画面上にリスト表示する際にはRecyclerViewやUITableViewなどに対応する差分更新機能(DiffUtilやUITableViewDiffableDataSource)などを用いて描画更新してあげて下さい。 一連の記事のまとめ ここまで様々な視点から長々と解説とライブラリの紹介をしてきました。 もちろん今回紹介したライブラリを使って頂いても構いませんが、最も大事なのは状態管理に対する考え方であり、アプリ内におけるデータの不整合が出づらく、ローカルキャッシュに振り回されない仕組みが用意できていることです。 それが達成されてさえいればどのような仕組みのでも構わないと思います。 データ取得処理のインターフェースをなるべく早く安定させて、変わらないようにしておくこともとても大事です。 その際に技術的詳細を隠蔽したり、取得先を抽象化することを意識してみてください。 一年後〜数年後に、きっと状態管理が少しだけ楽になっているはずです!
- 投稿日:2021-06-21T09:44:49+09:00
iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編)
iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編) iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(2/3 実装方針編) ← いまここ iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編) 前置き 前回「iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(1/3 考え方編)」という記事を書きました。 今回はその考え方をどうやって実際のコードに落とし込んでいくのかを整理しながら紹介するとともに、実際にSwift/Kotlin/Dartそれぞれ向けにリファレンスライブラリを作成したので合わせて紹介します。 記事内のコードはKotlinとSwiftの2つで記載します。 作成したリファレンスライブラリの紹介 前回・今回の記事の思想を元に実装したライブラリをKotlin版、Swift版、Dart版にわけて作成しました。 いずれもApache License 2.0にて提供しています。 上記ライブラリ群の提供する機能やインターフェースはほとんど同一です。 このことからも記事でお話する内容は特定の言語やフレームワークに依存するものではないことがわかるかと思います。 リファレンスとはいえ、Kotlin版については私が担当する数十万人が利用するAndroidアプリで実際に運用しているライブラリでもあります。 最初に申し上げておくと弊社プロダクトに必要な要件向けにライブラリまで落とし込んでみた一例であり、あらゆるシーンに対応するものではありません。 しかしながら、API通信とアプリ内キャッシングを行う一般的なアプリのユースケースでは十分だと考えています。 細かい思想や実装方針はいいからライブラリの使い方と使い勝手が知りたいんだと言う方は3/3 ライブラリ使い方編記事を御覧ください。 複雑さの軽減に必要な要素 前回の記事にてデータの取得先の抽象化や通知の仕組みが状態管理の複雑さの軽減につながるという話をしました。 ここで整理した内容を再度書き出してみます。 データの状態を表現できる構造が存在すること データに変更があった場合は教えてくれる仕組みを用意する(Observerパターンの概念) 使う側からキャッシュかAPIかなどどこから取ってきているかを意識させず整合性の取れた値を返す(Repositoryパターンの概念) 出来る限り早く値を返却する 使う側でデータを変数などで保持せず、取得先を常に1箇所に絞る(Single Source of Truthの概念) 最終的なインターフェース データ取得のための最終的な出力としては以下のような継続的に変更を受け取れる形がよさそうだという話も前回しました。 今回はこの形を吐き出すコードを実装を目指します。 Kotlin+Coroutines interface MyRepository { fun followUser(): Flow<State<User>> } Swift+Combine protocol MyRepository { func followUser() -> AnyPublisher<State<User>, Never> } ※今回はState自体がErrorの情報を持ちうるため、AnyPublisherの第2ジェネリクスはNeverを指定しています。 1. データの「状態」を扱う まずは整理した要素の「1. データの状態を表現できる構造が存在すること」をコードで表現してみます。 これは前回の記事に記載した通りですが、データの有無とは別にデータの状態を取り扱えるようにしておいたほうが良いです。 具体的には「停止状態(Fixed)・取得中状態(Loading)・エラー状態(Error)」の3つあればモバイルアプリの要件としては十分だと思います。 以下のようにデータの状態とデータの実体を組み合わせたState<T>というデータの箱を考えてみます。 Kotlin sealed interface State<out T> { val content: StateContent<T> data class Fixed<out T>(override val content: StateContent<T>) : State<T> data class Loading<out T>(override val content: StateContent<T>) : State<T> data class Error<out T>(override val content: StateContent<T>, val exception: Exception) : State<T> } sealed interface StateContent<out T> { data class Exist<out T>(val rawContent: T) : StateContent<T> class NotExist<out T> : StateContent<T> } Swift enum State<T> { case fixed(stateContent: StateContent<T>) case loading(stateContent: StateContent<T>) case error(stateContent: StateContent<T>, error: Error) } enum StateContent<T> { case exist(rawContent: T) case notExist } この実装例でデータの有無を?(Null Safety)ではなくsealed classやenumで表現しているのはわざとで、データが無いケースが存在するというのを強く意識してUIを実装してほしいがためです。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 2. Observerパターンの構築 次に「2. データに変更があった場合は教えてくれる仕組みを用意する」について実装を考えます。 データを保持して変化したときに通知できる仕組み、いわゆるObserverパターンを構築します。 KotlinではCoroutinesに含まれるStateFlowを、SwiftではCombineに含まれるCurrentValueSubjectを用いることでこの仕組みは簡単に実現できます。 RxJava,RxSwiftではBehaviorSubjectと呼ばれているものがこれに相当します。 Kotlin val observableData = MutableStateFlow<String>("initial_data") observableData.collect { data -> println("Data is changed: $data") } observableData.emit("new_data") Swift let observableData = CurrentValueSubject<String, Never>("initial_data") observableData.sink { data in print("Data is changed: \(data)") } observableData.send("new_data") 色々必要なコードは省いていますが、Swift・Kotlinともにこのようなコードで通知を記述するとデータを購読したタイミングで"initial_data"が通知され、emit,sendした時点で"new_data"を購読者に伝えることが出来ます。 通知の仕組みとデータの保存処理の分離 データを通知する仕組み自体は比較的簡単ですが、今回実装したライブラリのコードではデータの実体をこのような仕組みでは保持していません。 これはデータの実体の保持の仕組みをStateFlowやCurrentValueSubjectの振る舞いにロックインさせてしまうことを避けるためです。 StateFlowやCurrentValueSubjectに直接データをもたせるのは直感的ですが、これではアプリのキル時にデータが揮発してしまいます。 ではこの通知の仕組みを維持したままデータを永続化したい場合はどうすればよいでしょう? すでにAndroidのSharedPreferencesやiOSのCoreDataを使っていた場合は?これから使いたい場合は? それぞれのアプリの性質や歴史によってデータをどういった形式でどこに保存するかは様々です。 ゆえに今回は汎用的な用途を前提どこに保存するかは問わずに、データの変更通知の仕組みと分けて考えてみます。 データの実体と状態を別に管理する そこで着目するのが先の項目で話題に上げた「データの状態」です。 データの状態は一時的なものでアプリキル後まで永続化する必要は基本的にはありません。いかなる場合に揮発しても構わないはずです。 なのでこの要素を監視対象としてライブラリ内に組み込みStateFlowやCurrentValueSubjectにセットします。 データの実体はどこに保存してもよく、状態の変化の通知に合わせてついでに取ってきて一緒に返すくらいの考え方です。 これを満たすために、まず以下のように状態だけを取り扱う箱(DataState)を用意します。 エラー状態におけるのエラーの内容については基本的には揮発しても良いことが大半なので状態の一部として取り扱ってしまってよいでしょう。 Kotlin sealed interface DataState { class Fixed : DataState // 停止状態 class Loading : DataState // 取得中状態 class Error(val exception: Exception) : DataState // エラー状態 } Swift public enum DataState { case fixed // 停止状態 case loading // 取得中状態 case error(rawError: Error) // エラー状態 } これを内包するStateFlowやCurrentValueSubjectをシングルトンクラスで維持しつつ、通知される状態の流れをmap関数を使って変化させることで状態の変化に応じてデータを渡すことが出来るようになります。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) また、以下のコードはDataStateを通知のトリガーとして機能させつつキャッシュデータの実体と統合して前述したState<T>として返却する一例です。(※解説用に実際のコードより簡略化しています) Kotlin val dataStateObserver = MutableStateFlow<DataState>(DataState.Fixed()) // 状態を監視できるObserver fun observeData(): Flow<State<RawData>> { return dataStateObserver .map { dataState -> val data = loadCacheData() // 状態が変化した際に対象のデータの実体をキャッシュから取得する convertToState(data, dataState) // 最終的な出力形式である`State<T>`の形式に加工する } } Swift let dataStateObserver = CurrentValueSubject<DataState, Never>(.fixed()) // 状態を監視できるObserver func observeData() -> AnyPublisher<State<RawData>, Never> { dataStateObserver .map { dataState in let data = loadCacheData() // 状態が変化した際に対象のデータの実体をキャッシュから取得する return convertToState(data, dataState) // 最終的な出力形式である`State<T>`の形式に加工する } .eraseToAnyPublisher() } このようなアプローチを取ることで、データの保存先や保存形式に左右されずにデータ通知の仕組みを構築することが可能です。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) この手法を取る上で一つ注意すべきなのは、状態が変化したときしか通知されないためデータの実体を直接書き換えてしまうと通知されません。 データの更新を行う際は実体と状態を必ずセットで更新して上げる必要があります。 これらは手動で行おうとすると大変ですが、この2つの処理をセットにして共通化した仕組みを用意して必ずそこから更新処理を行うように徹底することでこの問題に対処することは可能です。作成したライブラリでもそのような仕組みを用意しています。 3&4, データ取得先の抽象化 次に考えてみるのは「3. 使う側からキャッシュかAPIかなどどこから取ってきているかを意識させず整合性の取れた値を返す」と「4. 出来る限り早く値を返却する」についてです。 これについては実際にコードに落とし込むにあたりどういった処理が必要なのか簡単にフローチャートを書いてみます。 上記の手順を踏んでデータを返すことで、ローカルキャッシュとリモートからのデータの取得を抽象化したデータを取り出すことが可能です。 さらにデータを要求者に返す仕組みを前述したObserverパターンと組み合わせることで、要求時のデータのみならず将来的な更新や状態の変化まで監視することが出来ます。 これを限界まで簡略化した上で愚直にコードで表現してみるとこんな感じかと思います。 データの入出力や状態の入出力については外部へ切り出して抽象化処理の共通部分のみ表現しています。 Kotlin fun process() { val currentState = loadState() // データの状態を取り出す if (currentState is DataState.Loading) return // 状態がLoadingなら何もしない val cacheData = loadDataFromCache() // キャッシュデータの実体を取り出す if (!needRefresh(cacheData)) return // キャッシュが有効なら何もしない saveState(DataState.Loading()) // データの状態をLoadingに変える (データが通知される) val response = fetchDataFromOrigin() // リモートから最新データを取得する if (response.isSuccess) { // データの取得に成功 saveDataToCache(response.data) // 取得したデータをキャッシュへ保存する saveState(DataState.Fixed()) //データの状態をFixedに変える (データが通知される) } else { // データの取得に失敗 saveState(DataState.Error()) //データの状態をErrorに変える (データが通知される) } } Swift func process() { let currentState = loadState() // データの状態を取り出す if case .loading = currentState { return } // 状態がLoadingなら何もしない let cacheData = loadDataFromCache() // キャッシュデータの実体を取り出す if !needRefresh(cacheData) { return } // キャッシュが有効なら何もしない saveState(.loading) // データの状態をLoadingに変える (データが通知される) let response = fetchDataFromOrigin() // リモートから最新データを取得する if response.isSuccess { // データの取得に成功 saveDataToCache(response.data) // 取得したデータをキャッシュへ保存する saveState(.fixed) //データの状態をFixedに変える (データが通知される) } else { // データの取得に失敗 saveState(.error) //データの状態をErrorに変える (データが通知される) } } 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 実際には非同期処理やパラメータによる制御処理を多数入れているので上記で示したコードとはかなり異なります。 5. Single Source of Truthを守る 最後に「5. 使う側でデータを変数などで保持せず、取得先を常に1箇所に絞る」について考えてみます。 とはいえ、これに関してはライブラリ等でどうにかすることは出来ないので利用者に頑張ってルールを守ってもらうしかありません。 今回紹介している仕組みは前回の記事からお話している通り、擬似的なSingle Source of Truthを実現していますが あくまでこの仕組みを通した場合に限定されるので、仕組みを介さずにデータを取得・更新した場合やこの仕組みの外側でデータを保持してしまった場合はこの前提は崩れてしまいます。 この仕組みを採用する以上は、「4. 出来る限り早く値を返却する」に基づくため、この仕組みの外側でデータをメンバー変数などで保持する必要はなく、常にこの仕組みを経由して取得・更新を行うことが十分可能な作りになっているはずです。 共通化出来ない部分を切り分ける ここまでデータの保存先や保存形式に影響を受けない部分の共通化処理を考えてきました。 ここからは前回の記事でもお話しましたが、共通化出来ない部分についてもう一度書き出してみます。 データの状態を保持する機構 キャッシュからの取得処理 キャッシュへの保存処理 API等のプライマリデータからの取得処理 キャッシュが有効か否かの判断処理(時間、個数、etc..) これらに関してはデータによって処理が変わる部分なので、それぞれの実装時に処理を記述を変えられるように枠組みだけを提供してみます。 具体的には以下のようなインターフェースの提供を検討します。(※解説用に実際のコードより簡略化しています) Kotlin interface StoreFlowableFactory<DATA> { fun loadDataFromCache(): DATA? // ローカルキャッシュからの取得処理 fun saveDataToCache(newData: DATA?) // ローカルキャッシュへの保存処理 suspend fun fetchDataFromOrigin(): DATA // リモートデータからの取得処理 fun needRefresh(cachedData: DATA): Boolean // ローカルキャッシュが有効かの判定処理 } Swift protocol StoreFlowableFactory { associatedtype DATA func loadDataFromCache() -> DATA? // ローカルキャッシュからの取得処理 func saveDataToCache(newData: DATA?) // ローカルキャッシュへの保存処理 func fetchDataFromOrigin() -> AnyPublisher<DATA, Error> // リモートデータからの取得処理 func needRefresh(cachedData: DATA) -> Bool // ローカルキャッシュが有効かの判定処理 } 上記のインターフェースに準じた実装をデータごとに用意してあげることで保存先や保存形式を実装した部分に任せつつ、その他の抽象化のための共通処理をまとめることが出来ます。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) また、これに加えて前述したデータの状態のみを取り扱う箱DataStateを通知の仕組みに乗せたシングルトンクラスも必要になります。 こちらについても共通化して隠蔽してしまうと柔軟性が損なわれるので今回のライブラリでは切り出していますが場合によっては共通化してしまっても良いかもしれません。 Kotlin abstract class FlowableDataStateManager { val dataState = MutableStateFlow<DataState>(DataState.Fixed()) } Swift open class FlowableDataStateManager { let dataState = CurrentValueSubject<DataState, Never>(.fixed()) } これらを継承して作成したクラスをシングルトンで保持する部分は隠蔽せず、実装側に任せます。 作成したライブラリの該当コードを次のとおりです。(Kotlin版 / Swift版) 複雑さを軽減する5つの要素をまとめる ここまでで、以下の5つの要素を実装に落とし込むための実装のパーツを紹介してきました。 今回はデータ通知の仕組みにKotlin Coroutines FlowやCombine Frameworkを使って表現しましたが、RxJavaやRxSwift、Stream APIやReactiveSwiftなど他の近しい技術を使っても実現できるはずです。 また、データの取得先を抽象化する処理についても解説したフローチャートのような分岐処理を記述することはさほど難しくはないと思います。 これらのパーツを組み合わせつつ、パラメータによる細かい調整を可能にしつつ共通化出来る部分を整えたのが今回紹介したStoreFlowableとなります。 ライブラリ内では様々なパターンを考慮して抽象化している部分が多々ありますが、特定のプロダクトに特化すればもっと簡易な仕組みを自作するだけでも十分機能すると思います。 次回記事に続く 今回の記事では考え方をコードに表現するにあたっての実装のキモとなる部分を作成したライブラリを元に紹介しました。 次回はライブラリの具体的な使い方について解説してみます。 次回記事: iOS/Androidアプリにおける状態管理の複雑さにリポジトリパターンを拡張して立ち向かう(3/3 ライブラリ使い方編)
- 投稿日:2021-06-21T02:30:56+09:00
iOSアプリのプロビジョニングプロファイル、証明書
iOSアプリ開発をすると、一度は証明書やプロビジョニングプロファイルでつまづいたことがあるのではないでしょうか。「iOS端末でアプリを実行するためには、証明書とプロビジョニングプロファイルが必要なことはわかったけれど、2つの違いは何なのか?」と疑問に感じたことが一度はあると思います。検索してみると、証明書、プロビジョニングプロファイルを作成する手順を解説しているブログ記事はありますが、その中身について詳しく解説している記事は少ないようです。本記事では、証明書、プロビジョニングプロファイルに格納されている情報や、その役割について解説します。 本記事では、秘密鍵、公開鍵、デジタル署名、証明書についての基本的な知識があることを前提としています。これらは広く使われている技術で、iOSに特化したものではありません。もしその知識がないようでしたら「暗号技術入門 第3版 秘密の国のアリス」の9章、10章の解説がわかりやすいので、そちらを読むことをおすすめします。それらの技術がiOS開発の証明書、プロビジョニングプロファイルにどのように使われているかを解説します。 実際の開発では、CLIで証明書やプロビジョニングプロファイルを作成してgitで管理できる match がよく使われています。その場合は記事で解説している知識は必ずしも必要ではありませんが、エラーが起きたときにエラーメッセージの内容を理解できなかったり、エラーを解決するのに時間がかかったりしてしまいます。iOSアプリの開発をするとプロジェクトの異動、証明書の更新、デバイスの追加などにより、半年に1回程度の頻度で証明書のエラーや実機ビルドができないなどで、対応が必要になることがあります。そのような場合に仕組みを理解しておくことが役に立つのでないかと考えています。 それでは証明書、プロビジョニングプロファイルを作成しながら、中身を確認していきます。 1. 公開鍵、秘密鍵、CSRファイルの作成 公開鍵、秘密鍵、CSRファイルを作成します。 キーチェーンアクセス -> 証明書アシスタント -> 認証局に証明書を要求 でキーチェーンに公開鍵と秘密鍵(下図)、指定したパスにCSRファイルが作成されます。 証明書情報 ユーザのメールアドレス: 自分のメールアドレス 通称: cer-test1 (ここではテスト用の名前にしています) ディクスに保存 鍵ペア情報を指定 鍵ペア情報 鍵のサイズ: 2048ビット アルゴリズム: RSA 公開鍵と秘密鍵は特に解説は不要かと思いますが、 CSRファイルについてはあまり馴染みがないのではないでしょうか。 openssl コマンドを使って中身を確認してみると、公開鍵とハッシュ値があることがわかります。CSRは、Certificate Signing Requestの頭文字で、Appleに署名を要求しています。 $ openssl req -in CertificateSigningRequest.certSigningRequest -noout -text Certificate Request: Data: Version: 0 (0x0) Subject: emailAddress=******, CN=cer-test1, C=JP Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: ...省略... Exponent: 65537 (0x10001) Attributes: a0:00 Signature Algorithm: sha256WithRSAEncryption ...省略... 2. CSRファイルのアップロード Apple Developer Programにログインして、Certificates, Identifiers & Profiles -> Certificates -> Apple Development、と進んで、CSRファイルをアップロードします。 3. 証明書のダウンロード CSRファイルをアップロードすると、証明書をダウンロードできるようになります。Macにダウンロードした証明書をダブルクリックするとキーチェーンアクセスの[自分の証明書]にインポートされます。 以下のコマンドで証明書の中身を見ると、Public-Key にアップロードした公開鍵があり、Issuer(証明書の発行者)がAppleであることが確認できます。 $ openssl x509 -inform der -in development.cer -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: ... Signature Algorithm: sha256WithRSAEncryption Issuer: CN=Apple Worldwide Developer Relations Certification Authority, OU=G3, O=Apple Inc., C=US Validity Not Before: May 24 12:41:46 2021 GMT Not After : May 24 12:41:45 2022 GMT Subject: UID=..., CN=Apple Development: ..., OU=G3, O=..., C=US Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: ...省略... // 1で作成した公開鍵 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical CA:FALSE X509v3 Authority Key Identifier: keyid:... Authority Information Access: CA Issuers - URI:http://certs.apple.com/wwdrg3.der OCSP - URI:http://ocsp.apple.com/ocsp03-wwdrg304 X509v3 Certificate Policies: Policy: 1.2.840.113635.100.5.1 User Notice: Explicit Text: Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements. CPS: https://www.apple.com/certificateauthority/ X509v3 Extended Key Usage: critical Code Signing X509v3 Subject Key Identifier: ... X509v3 Key Usage: critical Digital Signature 1.2.840.113635.100.6.1.2: critical .. 1.2.840.113635.100.6.1.12: critical .. Signature Algorithm: sha256WithRSAEncryption ...省略... それぞれの階層の意味はこちら https://www.ipa.go.jp/security/pki/033.html#_Toc3020768 3. p12ファイルの作成 一人で開発をするのであればこの手順は不要ですが、複数人や複数のMacで開発をする場合は証明書と秘密鍵を共有する必要があります。証明書はApple Developer Programからダウンロードできますが、秘密鍵はキーチェーンで秘密鍵を生成したMacにしかないのでダウンロードできません。そのため秘密鍵と証明書と一つにまとめたPKCS#12形式を使います。 キーチェーン -> 自分の証明書 -> 証明書を右クリックして、書き出すを選択 p12ファイルの中身をみると、証明書(CERTIFICATE)と秘密鍵(PRIVATE KEY)が含まれてていることが確認できます。 $ openssl pkcs12 -info -in cer.p12 -nodes MAC Iteration 1 MAC verified OK PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048 Certificate bag Bag Attributes friendlyName: Apple Development: ... localKeyID: ... subject=/UID=...735QQ/CN=Apple Development: .../OU=.../O=.../C=US issuer=/CN=Apple Worldwide Developer Relations Certification Authority/OU=G3/O=Apple Inc./C=US -----BEGIN CERTIFICATE----- ...省略... -----END CERTIFICATE----- PKCS7 Data Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048 Bag Attributes friendlyName: cer-test1 localKeyID: ... Key Attributes: <No Attributes> -----BEGIN PRIVATE KEY----- ...省略... -----END PRIVATE KEY----- 4. プロビジョニングプロファイルのダウンロード 次にprovisioning profileを作成します。作成のフローの中で証明書を選択する画面があるので、先ほどの証明書を選択します。 ダウンロードしたプロビジョニングプロファイルに、どんな情報があるのかを確認してみます。 $ security cms -D -i certest1.mobileprovision <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>AppIDName</key> <string>certest1</string> <key>ApplicationIdentifierPrefix</key> <array> <string>...</string> </array> <key>CreationDate</key> <date>2021-05-24T13:54:10Z</date> <key>Platform</key> <array> <string>iOS</string> </array> <key>IsXcodeManaged</key> <false/> <key>DeveloperCertificates</key> <array> <data>...省略...</data> </array> <key>Entitlements</key> <dict> <key>application-identifier</key> <string>...</string> <key>keychain-access-groups</key> <array> <string>....*</string> <string>com.apple.token</string> </array> <key>get-task-allow</key> <true/> <key>com.apple.developer.team-identifier</key> <string>...</string> </dict> <key>ExpirationDate</key> <date>2022-05-24T13:54:10Z</date> <key>Name</key> <string>certest1</string> <key>ProvisionedDevices</key> <array> <string>...</string> </array> <key>TeamIdentifier</key> <array> <string>...</string> </array> <key>TeamName</key> <string>...</string> <key>TimeToLive</key> <integer>365</integer> <key>UUID</key> <string>...e</string> <key>Version</key> <integer>1</integer> </dict> </plist> 上記ではマスクしてありますが、アプリケーションIDや有効期限などの情報があります。その他にDeveloperCertificates というKeyに開発者証明書があります。 <key>DeveloperCertificates</key> <array> <data>...省略...</data> </array> 3でダウンロードした証明書をpem形式に変換すると、プロビジョニングプロファイルの DeveloperCertificates の証明書と同じものです。そこから、プロビジョニングプロファイルの作成フローで選択した証明書が、プロビジョニングプロファイルに埋め込まれていることが確認できます。 // pem形式への変換 $ openssl x509 -in development.cer -out ios_development.pem -inform der コード署名 上記の手順で作成した証明書はコード署名で使います。コード署名については、ドキュメントでこのように解説されています。 Code signing (or signing) an app allows the system to identify who signed the app and to verify that the app has not been modified since it was signed. https://help.apple.com/xcode/mac/current/#/dev3a05256b8 のcode signsのポップアップ画面から引用 Code signing is a macOS security technology that you use to certify that an app was created by you. Once an app is signed, the system can detect any change to the app—whether the change is introduced accidentally or by malicious code. https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005929-CH1-SW1 から引用 ビルド ソースコードをビルドする際に、秘密鍵を使いコード署名をして、署名を検証できるように証明書も付けます。そうすることで、証明書(公開鍵)とそれに対応する秘密鍵を持つ開発者だけが、有効なコード署名ができます。 実行ファイルにどのように署名されているのかは、iOSのコード署名について で詳しく解説されています。 コード署名の検証 証明書の署名の検証、コード署名を検証することで、Appleに登録済みの開発者がアプリを作成したこと、アプリに改ざんがないことを確認できます。そうすることで、Appleが承認したアプリだけが実行できます。App Storeで配布しているアプリを改ざんして実行することはできません。 開発用のアプリの場合は上記のようになりますが、App Storeでアプリを配布する場合はもう少し複雑で、開発者がコード署名したものをAppleが再署名しています。What is the value of iOS code signing? ではその仕組みや問題点などが解説されています。 参考文献 What is app signing? About Code Signing
- 投稿日:2021-06-21T01:35:32+09:00
SwiftUI Tutorialsに登場したProperty Wrappersまとめ
はじめに 自分がApple公式のSwiftUI Tutorialsを通して学んだ時に登場したProperty Wrappersがそれぞれどういう役割かとかを忘れちゃいそうなので備忘のために書き残すことにしました。 watchOSやmacOSのAppを作る予定は現状なかったので、Chapter3までに登場したProperty Wrappersの紹介になります。 これから同じくSwiftUIを始めて勉強する方々の参考になればと思います。 Property Wrappersとは Swift5.1で実装された機能です。 プロパティのget/setに関わる制御を共通化するような仕組みです。 この後紹介する@StateやEnvironmentObjectはProperty Wrappersですし、自分で新たなものを定義することが可能です。 SwiftUI Tutorialsに登場したProperty Wrappers @State State | Apple Developer Documentation 通常SwiftUIのViewはstructで定義していくためプロパティの更新ができないが、@Stateをつけて宣言することでそのプロパティの値の読み書きができる。 @Stateで宣言したプロパティはViewのbodyもしくはViewから呼び出されるメソッドからのみアクセスする必要があるとAppleから推奨されているので、基本的にはprivateをつけて宣言するのが一般的のようだ。 Viewにプロパティの値を渡したい時は$を変数名につけることで実現できる。 struct ToggleView: View { /// トグルのスイッチの状態(初期値: false) @State private var isOn = false var body: some View { // isOnの値を監視($をつける!) Toggle(isOn: $isOn) { Text("スイッチを切り替える") } } } これで下記画像のようにスイッチの状態を切り替えることができるようになる。 @Publishedと@EnvironmentObject Published | Apple Developer Documentation EnvironmentObject | Apple Developer Documentation チュートリアルでは、ObservableObjectを準拠しているクラスのプロパティに@Publishedを付けることで監視側がデータの変更を取得できるようにしている。 @EnvironmentObjectをつけてプロパティを宣言することで、複数のViewに共通のインスタンスを渡して値を監視させられます。 下記コードでは親のContentView内で.environmentObject(User())でインスタンスを渡して子ビューであるTextViewとButtonsViewで値を参照しています。 final class User: ObservableObject { @Published var name = "Taro" @Published var age = 18 } struct ContentView: View { var body: some View { VStack { TextView() ButtonsView() } .environmentObject(User()) } } struct TextView: View { @EnvironmentObject var user: User var body: some View { Text("I'm \(user.name).") Text("I'm \(user.age) years old.") } } struct ButtonsView: View { @EnvironmentObject var user: User var body: some View { HStack { Button(action: { self.user.age += 1 }) { Text("歳をとる") } Button(action: { self.user.age -= 1 }) { Text("若返る") } } } } 実行したアプリでは、ボタンを押したときのユーザの年齢が動的に変更される。 これで一応動くのですが、environmentObjectはアプリ全体で共通使用するデータのやりとりをするので、正しくはContentViewから.environmentObject(User())を削除し、代わりにXXXApp.swift内に下記のようにして記述するのが適切なようです。 そうでないと、print文で確認すると更新はされているがアプリの見た目は変わらないというような、正常に動作しないことがありました。 XXXApp.swift struct SampleSwiftUIApp: App { var body: some Scene { WindowGroup { ContentView().environmentObject(User()) //ここで共通する } } } @StateObject StateObject | Apple Developer Documentation 一度初期化すると、フレームワーク側で@stateObjectがついたプロパティの値が保持され続けるのでビューの再描画が発生しても値が初期化されなくなります。 @ObservedObjectを使用していると初期化されてしまうので、再描画されても変わってほしくない場合は@stateObjectを使うと良さそうです。 チュートリアルを見る感じだと、こちらも初期化してそのままプロパティを保持して欲しいことを考えると起動直後に宣言して.environmentObjectで渡すのが適しているという感じでしょうか。 XXXApp.swift struct SampleSwiftUIApp: App { @StateObject var hogeData = HogeData() // 任意の保持され続けて欲しいデータ var body: some Scene { WindowGroup { ContentView().environmentObject(hogeData) // そのデータを渡す } } } @Binding Binding | Apple Developer Documentation データを格納するプロパティと、そのデータを変更/更新するビューを双方向に接続します。 ということは、@Bindingで設定したプロパティが更新されれば、それと接続している他のビューも更新されるということになるようです。 final class ToggleState: ObservableObject { @Published var isOn = false } struct ContentView: View { @EnvironmentObject var toggle: ToggleState var body: some View { VStack { ToggleAView(isOn: $user.isOn) ToggleBView(isOn: $user.isOn) } } } struct ToggleAView:View { @Binding var isOn: Bool var body: some View { Toggle("スイッチA", isOn: $isOn) } } struct ToggleBView:View { @Binding var isOn: Bool var body: some View { Toggle("スイッチB", isOn: $isOn) } } 上記のコードは同じプロパティをBindingしているので、片方のトグルを切り替えると、その状態がもう片方の状態にも同期されて切り替わるようになります。 @Environment Environment | Apple Developer Documentation EnvironmentValuesに定義されているビューの環境の設定値を取得/更新したいときに使います。 下記コードは取得の例です。 struct ContentView: View { @Environment(\.timeZone) var timeZone @Environment(\.calendar) var calendar @Environment(\.locale) var locale var body: some View { VStack { Button(action: { print(timeZone) // Asia/Tokyo (current) print(calendar) // gregorian (current) print(locale) //en (current) }) { Text("button") } } } } EnvironmentValuesは上記のようにtimeZoneやcalendar等の値意外にも豊富に定義されています。 また、自分でEnvironmentValuesの値を新たに定義することもできるそうです。 おわりに ひとまず、SwiftUI Tutorialsに登場するProperty Wrappersをそれぞれ調べてみて、それぞれがどういうものなのかはある程度把握できました。 他の記事を見る感じだと、今回紹介したものがやはりよく紹介されているのを見かけるので頻繁に活用していくのだと思われます。 これから実際にSwiftUIを使ったコーディングをしていくと思うので、これらを駆使しながら実装していきます。 Property Wrappersを複数種類組み合わせたときの挙動とかのケーススタディができていないのと、@ObservedObjectや@AppStorage等の他のProperty Wrappersのまとめができていなかったりするので、こういうときどれを使うのが良いかがまだ不明な状態ですが、また色々調べながら進めて行けたらと思います。 参照 【Swift 5.1】Property Wrappersとは? | 2速で歩くヒト SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは SwiftUIのProperty Wrappersとデータへのアクセス方法 EnvironmentValuesを制するものはSwiftUIを制する ※チュートリアルと各Property Wrappersのリンクは既に上記内で共有しているので省略させていただいています。