SwiftUIでカラーピッカーを作る連載記事です。前回まででノブを配置するところまで実装できていますが、現在値に関係無く、値が0のときの場所に固定表示されています。今回は、値によって表示位置を変更する処理を実装します。
表示位置の計算
この連載で実装しているカラーピッカーでは、現在値が0.0のときは左端、1.0のときは右端に表示するようにします。Y座標は中央揃えにします。この仕様を実装するためには、ノブの中心座標を次のような式で計算します。
X = ビューの幅 * 現在値
Y = ビューの高さ / 2PickerGradationViewのサイズを取得する
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を渡せば実現できます。
このコードを実行すると、次のようにプレビューが表示されます。期待通りの位置にノブが表示されます。













