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
に追加したバウンディングボックスの描画コードも不要になるので削除してください。