SwiftUIでカラーピッカーを作るという連載記事です。前回、現在値を表示するテキストフィールドを作りましたので、今回はその続きで、グラデーションビュー上に現在値を示すノブを表示する処理の見た目を実装します。
ノブの形状ですが、この記事では次のような形状にしたいと思います。
- 白い円に黒の縁取りを行う。
- X座標は左端を
0.0、右端を1.0として、現在値の位置に円の中心が来るように配置する。 - Y座標はグラデーションビューの高さに対して中央揃えとする。
SwiftUIで円を描画する
SwiftUIで円を描画するには、GeometryReaderとPathを組み合わせます。例えば次のようなコードです。
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 1)
.foregroundColor(.black)
}
}
}
これを実行すると、次のように表示されます。

白い円に黒の縁取りを行うは、次のような処理で実装できます。
- 黒い円を太めに描く
- 白い円を上から細めに描く
SwiftUIでは次のようなコードで実現できます。
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 10)
.foregroundColor(.black)
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 3)
.foregroundColor(.white)
}
}
}
このコードを実行すると、次のように表示されます。

グラデーションビュー上にインジケータを表示する
各チャネルのグラデーションビュー上にインジケータを表示しましょう。「SwiftUIで円を描画する」のコードを元に、インジケータの表示を実装します。
ColorPickerビューへのPickerIndicatorの追加
PickerIndicatorビューを追加し、コードは「SwiftUIで円を描画する」をのまま使用します。
その状態で、次のように、ColorPickerに各チャネルのグラデーションビューの中心にPickerIndicatorを表示するコードを追加します。次のようなコードになります。
struct ColorPicker: View {
var redStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var redEndColor: Color = Color(red: 1.0, green: 0.0, blue: 0.0)
var greenStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var greenEndColor: Color = Color(red: 0.0, green: 1.0, blue: 0.0)
var blueStartColor: Color = Color(red: 0.0, green: 0.0, blue: 0.0)
var blueEndColor: Color = Color(red: 0.0, green: 0.0, blue: 1.0)
@StateObject var channelValue = ColorPickerChannelValue()
var body: some View {
VStack {
HStack {
Text("Red: ")
.font(.title)
TextField("", text: channelValue.redDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
HStack {
Text("Green: ")
.font(.title)
TextField("", text: channelValue.greenDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
HStack {
Text("Blue: ")
.font(.title)
TextField("", text: channelValue.blueDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
}
}
.padding()
}
}
PickerGradationViewにPickerIndicatorビューを重ねるので、ZStackの中にPickerGradationViewを移動しました。
PickerIndicatorビューはデフォルトの状態では大きすぎるので、frameモディファイアを使ってちょうど良いサイズに変更しています。
ZStackはデフォルトの状態だと中央揃えになります。引数alignmentに.leadingを指定して、左揃え(先頭揃え)に変更しました。
このコードを実行すると、次のように表示されます。

PickerIndicatorのゼロ位置の調整
プレビューをよく見てください。PickerIndicatorの位置が右に若干ずれているのが分かると思います。
PickerIndicatorに次のように// バウンディングボックスを描く以下のコードを追加してください。GeometryReaderのバウンディングボックスを描画します。
struct PickerIndicator: View {
var body: some View {
GeometryReader { geometry in
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 10)
.foregroundColor(.black)
Path { path in
let minLength = min(geometry.size.width, geometry.size.height)
path.addEllipse(in: CGRect(x: 0, y: 0, width: minLength, height: minLength))
}
.stroke(lineWidth: 3)
.foregroundColor(.white)
// バウンディングボックスを描く
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: geometry.size.width - 1, y: 0))
path.addLine(to: CGPoint(x: geometry.size.width - 1, y: geometry.size.height - 1))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height - 1))
path.addLine(to: CGPoint(x: 0, y: 0))
}
.stroke(lineWidth: 1)
.foregroundColor(.yellow)
}
}
}
バウンディングボックスの左端がZStackによる整列位置になっていることが分かると思います。

PickerIndicatorは値が0で左端にあるときは、PickerIndicatorの中心がPickerGradationViewの左端になる必要があります。そのためには、PickerIndicatorの幅の半分だけ左に位置をずらす必要があります。
位置の調整にはoffsetモディファイアを使用できます。次のようにPickerIndicator()にoffsetモディファイアを追加します。
ZStack(alignment: .leading) {
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: -10)
}
このコードを実行すると、次のように表示され正しい位置に表示されると思います。また、PickerIndicatorに追加したバウンディングボックスの描画コードも不要になるので削除してください。













