
SwiftUIで動的なスタンプカードを実装する
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
本記事のAndroid版は「Jetpack Composeで動的なスタンプカードを実装する」をご覧ください。
パンに付いてくるシールを集めて白いお皿と交換するなど日常的な商取引でもスタンプカードを利用していることは多い。
コンシューマ向けアプリを開発していると、それと同様に来店回数やランキングによってスタンプを付与し、一定数のスタンプと商品とを交換する機能を実装することがある。たとえば、フィットネスアプリの場合、ユーザーが定められた運動目標を達成するごとにスタンプを付与し、一定数を集めるとヘルスケア製品の割引などの報酬を得られる。
この記事では、LazyVGridを使用して簡単なスタンプカードを実装する方法を紹介する。

スタンプカードの実装
スタンプカードのようにスタンプを等間隔で並べて表示するにはLazyVGridが適している。LazyVGridの使い方についてはリルオッサ氏の「【SwiftUI】LazyVGridを使って簡単なSF Symbolsのカタログアプリを作る」が参考になる。
以下はスタンプをグリッドレイアウトで表示する画面の実装例である。現在16個中2個のスタンプを獲得した状態を表現している。
スタンプのViewを実装する
各スタンプは色でアクティブか非アクティブかを区別する。まず、アクティブなスタンプViewを実装する。指定された色で塗りつぶされた円をユーザーが獲得したスタンプとする。
struct ActiveStampView: View {
    let text: String
    let stampColor: Color
    var body: some View {
        Circle()
            .fill(stampColor)
            .overlay(
                Text(text)
                    .font(.caption)
                    .foregroundColor(.white)
            )
    }
}
次に、非アクティブなスタンプViewを実装する。このViewはスタンプ未獲得状態を破線の円で示す。
struct InactiveStampView: View {
    let text: String
    let stampColor: Color
    var body: some View {
        Circle()
            .stroke(
                stampColor,
                style: StrokeStyle(
                    lineWidth: 2,
                    lineCap: .round,
                    lineJoin: .round,
                    dash: [10, 5]
                )
            )
            .overlay(
                Text(text)
                    .font(.caption)
                    .foregroundColor(stampColor)
            )
    }
}
スタンプ画面の実装
16個中2個のスタンプを獲得のような状態を表現するために、num: 2とrequired: 16をパラメータとする StampScreen を実装した。
struct StampScreen: View {
    /// 現在のスタンプ数
    let num: Int
    /// 必要なスタンプ数
    let required: Int
    /// アクティブなスタンプ色
    let activeStampColor: Color
    /// 非アクティブなスタンプ色
    let inactiveStampColor: Color
    init(
        num: Int,
        required: Int,
        activeStampColor: Color = Color.red,
        inactiveStampColor: Color = Color.gray
    ) {
        self.num = num
        self.required = required
        self.activeStampColor = activeStampColor
        self.inactiveStampColor = inactiveStampColor
    }
    private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 7)
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.flexible())]) {
                Text("\(num) / \(required)")
                    .font(.title)
                    .foregroundColor(.black)
            }
            LazyVGrid(columns: columns, alignment: .center) {
                ForEach(0 ..< required, id: \.self) { index in
                    if index < num {
                        // アクティブなスタンプ
                        ActiveStampView(
                            text: "\(index + 1)",
                            stampColor: activeStampColor
                        )
                    } else {
                        // 非アクティブなスタンプ
                        InactiveStampView(
                            text: "\(index + 1)",
                            stampColor: inactiveStampColor
                        )
                    }
                }
            }
            .padding(.horizontal, 16)
        }
    }
}
#Preview {
    NavigationView {
        StampScreen(num: 2, required: 16)
    }
}
上記のサンプルコードを実行すると、下図のようなスタンプカードを実装できる。

ActiveStampView や InactiveStampViewの実装を変更することで、ハート型や星形など様々な形状のViewへの対応や、アプリバイナリにスタンプ画像をリソースとして組み込んでおき利用したり、サーバーからの画像をダウンロードして表示させたりすることも実現できるだろう。
まとめ
SwiftUIでは、LazyVGrid や LazyHGridを活用して、グリッドレイアウトのViewを簡単に実装することができる。スタンプカードのようなにスタンプを規則正しく並べて表示するなどの用途に向いている。またサンプルコードで示したようにScrollViewに複数のLazyVGridを組み込むことで、1列のアイテムと7列のアイテムを混在させて並べられる。おそらくUICollectionViewを使った実装よりも直感的に扱うことができるだろう。














