サイボウズのiOSプロダクトで利用しているProperty WrapperをOSSとして公開しました!

はじめに

こんにちは、モバイルエンジニアのオジマです。

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の機能により、showingAlertshowingSheetが正しく排他制御されます。そのため、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