SwiftUIでカラーピッカーを作る連載記事です。前回まででノブを配置するところまで実装できていますが、現在値に関係無く、値が0のときの場所に固定表示されています。今回は、値によって表示位置を変更する処理を実装します。
表示位置の計算
この連載で実装しているカラーピッカーでは、現在値が0.0
のときは左端、1.0
のときは右端に表示するようにします。Y座標は中央揃えにします。この仕様を実装するためには、ノブの中心座標を次のような式で計算します。
X = ビューの幅 * 現在値
Y = ビューの高さ / 2
PickerGradationViewのサイズを取得する
PickerGradationView
のサイズを取得する処理を実装します。SwiftUIで任意のビューの大きさを取得する方法については次の記事を参照してください。
取得したサイズを入れるプロパティを追加する
PickerGradationView
のサイズを代入するプロパティをColorPicker
に追加します。次のようにColorPicker.swift
にコードを追加します。
import SwiftUI
struct ColorPicker: View {
// 省略 ...
@StateObject var channelValue = ColorPickerChannelValue()
@State var redGradationViewSize = CGSize()
@State var greenGradationViewSize = CGSize()
@State var blueGradationViewSize = CGSize()
3つのPickerGradationViewのサイズを取得する
PickerGradationView
のサイズを動的に取得して、追加したプロパティに代入する処理を実装します。関連記事にあるように、background
モディファイアで背景にGeometryReader
を配置してサイズを調べます。
取得したサイズをプロパティに代入するとSwiftUIによってビューの再構築が行われます。再構築中に別の再構築を始めることはできないので、DispatchQueue
で遅延処理します。再構築回数を最小限にするため値のチェックも行って、変更されているときのみ代入します。
PickerGradationView(startColor: redStartColor, endColor: redEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != redGradationViewSize {
redGradationViewSize = geometry.size
}
}
}
}
}
このコードはredGradationViewSize
のコードです。同様のコードをgreenGradationViewSize
とblueGradationViewSize
に対しても実装します。実装後の全体のコードはこのページの後ろにある「実装後のコード全体」を参照してください。
現在値でノブを移動する
現在値でノブを移動する処理を実装します。ノブの移動はPickerIndicatorビューのoffsetモディファイアに渡す座標を変更することで行います。
X座標の計算処理の実装
チャネル毎にノブのX座標を計算するコンピューテッドプロパティを追加します。次のようにコードを追加します。
import SwiftUI
struct ColorPicker: View {
// 省略
var redIndicatorX: CGFloat {
redGradationViewSize.width * channelValue.red - 10
}
var greenIndicatorX: CGFloat {
greenGradationViewSize.width * channelValue.green - 10
}
var blueIndicatorX: CGFloat {
blueGradationViewSize.width * channelValue.blue - 10
}
第3回 現在値のノブを表示するで書いたように、PickerIndicator
ビューの幅の半分だけずらす必要があるので、-10
した値を返すようにしています。
PickerIndicator.offsetに渡す
PickerIndicatorビューのoffsetモディファイアに渡す値を、追加したコンピューテッドプロパティに変更します。次のようなコードになります。
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: redIndicatorX)
このコードはRedチャネル用です。同様の変更をGreenチャネル、BlueチャネルのPickerIndicatorビューにも行います。
実装後のコード全体
実装後のコード全体は次のようになります。
import SwiftUI
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()
@State var redGradationViewSize = CGSize()
@State var greenGradationViewSize = CGSize()
@State var blueGradationViewSize = CGSize()
var redIndicatorX: CGFloat {
redGradationViewSize.width * channelValue.red - 10
}
var greenIndicatorX: CGFloat {
greenGradationViewSize.width * channelValue.green - 10
}
var blueIndicatorX: CGFloat {
blueGradationViewSize.width * channelValue.blue - 10
}
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)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != redGradationViewSize {
redGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: redIndicatorX)
}
HStack {
Text("Green: ")
.font(.title)
TextField("", text: channelValue.greenDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: greenStartColor, endColor: greenEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != greenGradationViewSize {
greenGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: greenIndicatorX)
}
HStack {
Text("Blue: ")
.font(.title)
TextField("", text: channelValue.blueDisplayString)
.textFieldStyle(.roundedBorder)
}
ZStack(alignment: .leading) {
PickerGradationView(startColor: blueStartColor, endColor: blueEndColor)
.frame(height: 100)
.background {
GeometryReader { geometry in
Path { path in
DispatchQueue.main.async {
if geometry.size != blueGradationViewSize {
blueGradationViewSize = geometry.size
}
}
}
}
}
PickerIndicator()
.frame(width: 20, height: 20)
.offset(x: blueIndicatorX)
}
}
.padding()
}
}
表示確認
意図したように表示されるか確認します。ちょうど、3つチャネルがあるので、Redチャネル=0.0, Greenチャネル=0.5, Blueチャネル=1.0のプレビューが表示されるように、ColorPicker.swift
のColorPicker_Previews
のコードを変更します。
struct ColorPicker_Previews: PreviewProvider {
static var channelValue: ColorPickerChannelValue {
let value = ColorPickerChannelValue()
value.red = 0.0
value.green = 0.5
value.blue = 1.0
return value
}
static var previews: some View {
ColorPicker(channelValue: channelValue)
.previewInterfaceOrientation(.portrait)
}
}
各チャネルの値はColorPicker.channelValue
プロパティから取得されるので、ColorPicker_Previews
がColorPicker
ビューを作るときに表示したい値を代入したColorPickerChannelValue
を渡せば実現できます。
このコードを実行すると、次のようにプレビューが表示されます。期待通りの位置にノブが表示されます。