はじめに
こんにちは、モバイルエンジニアのオジマです。
Swiftにはバージョン 5.1からProperty Wrapperという強力な言語機能が追加されています。SwiftUIの@State
などでも馴染みが深いのではないでしょうか。
Property Wrapperは@State
などのすでにAppleが用意しているもの以外にも、自身でオリジナルなProperty Wrapperを作成し利用することができます。私が携わっているプロダクトでも自作のProperty Wrapperを利用しています。
今回、サイボウズのiOSプロダクトで利用している自作のProperty Wrapperのうち2つをOSSとして公開しました。 この記事では、公開したそれぞれのProperty Wrapperの利用方法について紹介します。
CachePropertyKit
概要
CachePropertyKitはin-memoryなキャッシングを手軽に行えるライブラリです。CachePropertyKitは以下の機能を提供します。
@Cache
キャッシュを行うプロパティに付与するProperty Wrapper。
CacheContainer
キャッシュしたデータを外部から操作するためのクラス。
利用例
それぞれの利用方法を紹介します。
@Cache
@Cache
は付与したプロパティをキャッシュする機能を備えたProperty Wrapperです。引数として、キャッシュしたデータを識別するためのキーと、キャッシュの生存期間の2つを与えることができます。キーは再度キャッシュした値を参照する時や削除する時に利用します。生存期間は時間間隔指定(.duration(_:)
)と日付指定(.until(_:)
)の2種類が利用できます。
以下のコードは@Cache
の利用例です。メソッド内のローカルプロパティやインスタンスプロパティなど任意の場所で利用できます。
struct Repository { func fetch() async throws -> Data { // 値を代入してから3600秒の間、代入された値をキャッシュする // 3600秒以降はキャッシュがクリアされnilを返す @Cache(key: "repository_data", lifetime: .duration(3600)) var data: Data? if let data { return data } else { let newData = try await fetchNewData() data = newData return newData } } }
CacheContainer
CacheContainer
を用いることで@Cache
の引数で指定した生存期間によらず、キャッシュを任意のタイミングでクリアすることができます。ログアウト時にキャッシュしたユーザー情報を削除したい時などに利用できます。
利用は以下のコードのように行います。このコードではプロダクト内でキャッシュした全てのキャッシュをクリアしていますが、キャッシュ時に指定したキーを用いて特定のキャッシュをクリアすることも可能です。
struct ContentView: View { var body: some View { MyView() .task { for await _ in logoutSucceeded { CacheContainer.clearAll() } } } }
ExclusivePresentationState
概要
SwiftUIを用いてシートやアラートを出す際、気をつけておかないと表示状態を司るフラグが衝突してしまい、期待通りの挙動にならない場合があります。iOSのバージョンの違いによって、あるバージョンでは期待通りではないがこちらのバージョンでは期待通りになる場合も存在します。iOS 16では以下のコードで作られたViewに対して次の操作を行うと、シートが表示できなくなります。
①アラートを表示するボタンを押してアラートを表示する ②5秒間待つ ③アラートを閉じる ④シートを表示するボタンを押す
struct ContentView: View { @State private var showingAlert = false @State private var showingSheet = false var body: some View { VStack { Button("Showing Alert") { showingAlert = true } Button("Showing Sheet") { showingSheet = true } } .alert("Alert", isPresented: $showingAlert, actions: {}) .sheet(isPresented: $showingSheet) { Text("Sheet") } .task { try? await Task.sleep(for: .seconds(5)) // ユーザー操作以外でシートの表示が発火する場合(プッシュ通知タップなど)の擬似コード showingSheet = true } } }
ExclusivePresentationStateはこの問題を解決するライブラリです。 ExclusivePresentationStateは以下の機能を提供します。
@ExclusivePresentationState
排他な状態管理を実現するProperty Wrapper。
ExclusivePresentationStateContainer
@ExclusivePresentationState
で管理している状態を外部から操作する構造体。
利用例
@ExclusivePresentationState
の利用シーンは@State
と基本的に一緒です。複数の状態間で排他制御を行いたい場合に、それぞれの状態にこのProperty Wrapperを付与します。引数として、排他制御をしない範囲を表すグループキーと、排他制御の優先度を与えることができます。
先ほどのiOS 16で期待通りに動作しないコードを@ExclusivePresentationState
を用いて書き換えると以下のようになります。以下のコードでは@ExclusivePresentationState
の機能により、showingAlert
とshowingSheet
が正しく排他制御されます。そのため、showingAlert
がtrueの場合、つまりアラートが表示されている場合にshowingSheet
がtrueになることはないので、常に期待通りの動作を得ることができます。
import ExclusivePresentationState struct ContentView: View { @ExclusivePresentationState(priority: .high) private var showingAlert = false @ExclusivePresentationState(priority: .low) private var showingSheet = false var body: some View { VStack { Button("Showing Alert") { showingAlert = true } Button("Showing Sheet") { showingSheet = true } } .alert("Alert", isPresented: $showingAlert, actions: {}) .sheet(isPresented: $showingSheet) { Text("Sheet") } .task { try? await Task.sleep(for: .seconds(5)) // ユーザー操作以外でシートの表示が発火する場合(プッシュ通知タップなど)の擬似コード showingSheet = true } } }
まとめ
この記事ではサイボウズがOSSとして公開したProperty Wrapperについて紹介しました。今回は実装についてあまり触れませんでした。気になる方はぜひリポジトリを覗いてみてください。
現在実装されている機能は必要最低限のものばかりです。改善や機能追加のアイディアを思いついた際は、コミットいただけると幸いです。
CachePropertyKit: https://github.com/cybozu/CachePropertyKit
ExclusivePresentationState: https://github.com/cybozu/ExclusivePresentationState