第4回 値によってノブの表示位置を変更する | SwiftUIでカラーピッカーを作る

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のコードです。同様のコードをgreenGradationViewSizeblueGradationViewSizeに対しても実装します。実装後の全体のコードはこのページの後ろにある「実装後のコード全体」を参照してください。

現在値でノブを移動する

現在値でノブを移動する処理を実装します。ノブの移動は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.swiftColorPicker_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_PreviewsColorPickerビューを作るときに表示したい値を代入したColorPickerChannelValueを渡せば実現できます。

このコードを実行すると、次のようにプレビューが表示されます。期待通りの位置にノブが表示されます。

ライブプレビューの実行結果
ライブプレビューの実行結果

連載記事一覧

この記事は「SwiftUIでカラーピッカーを作る」という連載の記事です。連載の他の記事はこちらです。

SwiftUIでカラーピッカーを作る 連載記事一覧

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

アプリ開発が好きなアプリ開発者。このブログは学習メモを記事にしたテックブログです。仕事ではアプリ開発をメインに、技術書の執筆やセミナーの講師などもしています。業務や著書のサイトはこちらです→ アールケー開発

目次