概要
よくあるレビュー用の星の実装をSwiftUIでしてみました。
4.3/5
の様なキリの悪いレートも表示できます。
タップ、スライドでユーザがレートを操作することも可能です。
動作環境
iOS16 以降
動作
実装
import SwiftUI
@available(iOS 16.0, *)
public struct ReviewRateView: View {
@Binding var reviewRate: CGFloat
let eachStarSize: CGFloat
let maximumStar: CGFloat
let space: CGFloat
let viewWidth: CGFloat
let range: Range<Int>
public init(
reviewRate: Binding<CGFloat>, // 0.0 〜 1.0
eachStarSize: CGFloat = 40,
maximumStar _maximumStar: Int = 5,
space: CGFloat = 5
) {
_reviewRate = reviewRate
self.eachStarSize = eachStarSize
self.maximumStar = CGFloat(_maximumStar)
self.space = space
self.viewWidth = eachStarSize * maximumStar + (space * (maximumStar - 1))
self.range = 0 ..< Int(maximumStar)
}
@ViewBuilder
public var body: some View {
ZStack {
Color.gray
.frame(width: viewWidth, height: eachStarSize)
.overlay(alignment: .leading) {
Color.yellow.frame(width: viewRatio * viewWidth, height: eachStarSize)
}
.mask {
HStack(spacing: space) {
ForEach(Array(range), id: \.self) { _ in
Image(systemName: "star.fill")
.resizable()
.scaledToFit()
.frame(width: eachStarSize)
}
}
}
.onTapGesture(perform: { point in
updateReviewRate(from: point.x)
})
.gesture(
DragGesture(minimumDistance: 0)
.onChanged({ value in
updateReviewRate(from: value.location.x)
})
.onEnded({ value in
updateReviewRate(from: value.location.x)
})
)
}
}
var viewRatio: CGFloat {
let maxStarRatio = (eachStarSize * maximumStar) / viewWidth
let eachSpaceRatio = space / viewWidth
let spaceRatio = eachSpaceRatio * CGFloat(Int(reviewRate * maximumStar))
let viewRatio = spaceRatio + (reviewRate * maxStarRatio)
return min(1, max(viewRatio, 0))
}
func updateReviewRate(from pointX: CGFloat) {
let _reviewRate = CGFloat(max(1, Int(pointX / (eachStarSize + space)) + 1))
withAnimation(.spring) {
reviewRate = _reviewRate / maximumStar
}
}
}
使う側のコード
@available(iOS 16.0, *)
public struct ContentView: View {
@State var reviewRate1: CGFloat = 4.3 / 5
@State var reviewRate2: CGFloat = 5 / 8
@State var reviewRate3: CGFloat = 1.5 / 3
public init() {
}
public var body: some View {
VStack {
ReviewRateView(reviewRate: $reviewRate1)
ReviewRateView(reviewRate: $reviewRate2, eachStarSize: 20, maximumStar: 8, space: 10)
// 操作不可にする場合
ReviewRateView(reviewRate: .init(get: { reviewRate3 }, set: { _ in }), eachStarSize: 20, maximumStar: 3, space: 30)
}
}
}
実装のポイント
-
黄色の四角いバーに星でマスクすることで実現しています。
-
以前UIKit同じようなUIを実装したときは半分のスターの画像を用意したり、各それぞれの星画像にタップイベントを仕込んだりして煩雑なコードになってた気がします。
SwiftUIではアニメーション、ユーザーインタラクションも含めて70行程度で非常にクールに書けました。
ただ、レートを描画するとき、その割合をそのまま表示するのではなく、スペースが占領する割合も考慮して計算したところは思ったより複雑になりました。 -
全体のサイズは星のサイズとスペースから割り出されるので、可変にするためには外側でGeometoryReaderなどでもろもろ計算して渡してあげる必要があるでしょう。