SwiftUIでカスタム評価ビューを自作する

こんにちは、 @toshi0383 です。
Globee アドベントカレンダー5日目の記事です。
昨日は @tnishida による[Adobe Max 2023](https://qiita.com/tnishida/items/7c671c73d2d41409be76)でした。
本日はSwiftUIネタにしてみました。

# 5つ星評価ビュー

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/dc635d33-7bd0-3939-d945-af487e2847ea.png)


このようなカスタム評価ビューはアプリ開発ではよく出てくると思いますが、皆さんのプロジェクトではどのように実装されていますか？
abceed iOSアプリではCosmosというライブラリで実装されているようでした。
https://github.com/evgenyneu/Cosmos

このライブラリは最終更新が数年前でかなり古く、バグっている箇所もありますので、前からいい感じの実装で置き換えたいと思っていました。
今回は自前で実装してみましたので、記事にしました。

## 目標

目標は、星を水平に5つ並べて、ユーザーが左右にドラッグすることで0.0から5.0までの評価を0.5刻みで入力できるようにすることです。
インターフェースとしては以下のような形を期待しています。

```swift
StarRatingView { (value: Float) in }
```

少し実装をイメージしてみると、指がどこまで行ったら0.5増減させるのかなど、自分で作ろうと思うと意外と難しい予感がしました。
皆さんならどのように実装しますか？よかったら少しスクロールを止めて考えてみてください。

正解はひとつではないと思いますが、以下私の場合ということで解説していきます。

## コンポーネント

まず、星の塗り方は3種類ですので、3つの星のビューを別々に定義することにしました。
この方が可読性の観点でも良さそうと考えました。

- 半分塗りつぶされた星（`HalfStar`）
- 完全に塗りつぶされた星（`FullStar`）
- 塗りつぶされていない星（`EmptyStar`）

これらを `HStack` に並べて、 `DragGesture` をつけて計算、状態に反映するというイメージです。

## 実装

さて、皆さん星の形をしたViewって実装したことありますか？いわゆる五芒星です。
`Shape` を使うのはいいとして、どういう計算をしたらいいのか、多くの方は悩むのではないでしょうか。
私もそうでした。

というわけで、おもむろにChatGPT-4を立ち上げて上記仕様を丸投げするわけですね。
「仕様これだから、あとよろしく」と。
一瞬で答えが出てきたので、Playgroundにコードを貼り付けて実行してみました。
結果は..

### Binary operator '/' cannot be applied to operands of type 'CGFloat' and 'Float'

ちょっとしたコンパイルエラーがありました。
まあこれくらいは全然許容ですよね。
エラーコードをそのままChatGPTに投げたらすぐに修正版が上がってきます。

### 星ではなく三角になっている

プロンプトで星の定義をきちんとした方がよかったでしょうか。
「ここでは実際には星を描画してください」というコメント付きでダミー実装で済ませてきたようです。
気を取り直して「五芒星にしてほしい」と伝えました。

### まだ星になっていない

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/8483cad9-d0e5-0d4f-dce8-d9090fe10989.png)

全然直りません。もっとグチャグチャになっちゃいました。
ちょっと六芒星っぽいです。
以降何度かやりとりしましたが埒が開かないので、別の方法を模索しました。

たまたま、`UIBezierPath`を使うコードがStackOverflowに転がっていました。
https://stackoverflow.com/questions/38343458/how-to-make-star-shape-with-bezier-path-in-ios

ちょっと格好悪いかもしれませんが、試しにこれをそのままChatに投げて、Shapeにしてもらいましょう。
無事に星が描画されました。このようなコードになっています。

```swift
struct StarShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        let originalSize = CGSize(width: 88.29, height: 80.5)
        let scale = min(rect.width / originalSize.width, rect.height / originalSize.height)

        let points = [
            CGPoint(x: 45.25, y: 0),
            CGPoint(x: 61.13, y: 23),
            CGPoint(x: 88.29, y: 30.75),
            CGPoint(x: 70.95, y: 52.71),
            CGPoint(x: 71.85, y: 80.5),
            CGPoint(x: 45.25, y: 71.07),
            CGPoint(x: 18.65, y: 80.5),
            CGPoint(x: 19.55, y: 52.71),
            CGPoint(x: 2.21, y: 30.75),
            CGPoint(x: 29.37, y: 23)
        ]

        let scaledPoints = points.map { CGPoint(x: $0.x * scale, y: $0.y * scale) }

        path.move(to: scaledPoints[0])
        for point in scaledPoints.dropFirst() {
            path.addLine(to: point)
        }
        path.closeSubpath()

        return path
    }
}
```

やはりちょっと納得いかないのですが、星になったからよしとしましょう。
しかし、まだ完成していません。

### HalfStarの塗りがおかしい

`HalfStar` の見た目がちょっとどうしようもないくらいダメだったので、ここは自分で工夫してみました。
`EmptyStar`と`FullStar`を`ZStack`で重ね合わせ、`FullStar`の半分だけを表示することで実現しました。
色紙で工作している気分ですね。
今回自分でコード書いたのはここだけです。
なんということでしょう。

```swift
struct HalfStar: View {
    var body: some View {
        ZStack(alignment: .center) {
            EmptyStar()
                .foregroundColor(.yellow)
            FullStar()
                .foregroundColor(.gray)
                .mask(alignment: .center) {
                    HStack(spacing: 0) {
                        Color.clear
                        Rectangle()
                    }
                }
        }
    }
}
```

## 完成

そんな感じで1時間くらいでしょうか？
ギリギリ業務で使えそうなレベルのものができました。
`frame` 指定することで任意の高さにスケールしてくれます。
`DragGesture`は最初からいい感じに動いていました。
星を黄色以外にしたい等の要望もありそうなので `color` などの引数を受け取っても良さそうですね。
デザインの指摘が入ることを見越して、 `spacing` だけは設けておきました。
あとは`onChangeValue`でHaptic Feedbackがあると嬉しいかもしれませんね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/2212133f-c35b-16cb-c50e-392528f73b9f.png)

動画が貼れないのでこちらのリポジトリもCloneして遊んでみてください。
https://github.com/toshi0383/StarRatingView

## まとめ

SwiftUIの強力なカスタマイズ機能を使用して、ユーザーインタラクションに富んだ評価ビューを作成することができました。
プロトタイプするには本当にChatGPTは便利ですね。
今度やる時には`StarShape` も`HalfStar`も一発でいい感じに作ってくれるようになっているんではないでしょうか。
皆さんもぜひ、ChatGPTを活用して時短実装したUIなどあればシェアしてみてください。

なお`StarShape`の実装には納得がいっていませんので、続編で正式にプロのエンジニアっぽい実装にします。
ご期待ください。

ちなみにこの記事もその時のやり取りの最後に「今回の話技術ブログにして」とお願いして出してもらったものをベースに書き上げました。
ゼロから書くよりも楽に感じます。
こちらもぜひトライしてみてください。

それでは。

# 採用情報
現在株式会社Globeeではエンジニア採用を積極的に行っています。
まずはカジュアル面談にて弊社の「教育」にかける思いや「ものづくり」の考え方についてお話できればと思いますので、皆様お気軽にご応募ください。
https://globee.notion.site/Globee-d184eefc3561480292481299c64b04bc