SwiftUIでカラーピッカーを作る連載記事です。前回までの実装ではモデルがチャネル毎、更にグラデーションの情報とチャネルの現在値の情報が分かれていて、動的にグラデーションを変化させる処理の実装を困難にしています。今回はこれらを統合して、カラーピッカー全体の情報を持つモデルクラスに作り変え、同時にグラデーションを動的に変更する処理を実装します。
グラデーションの動的変更
前回までの実装でグラデーションは各チャネルの値を0.0
から1.0
まで変化させた状態を表示しています。このときに作成する色は対象のチャネル以外のチャネルの値を0.0
に固定しています。
これを0.0
に固定せず、各チャネルの現在値を使用する様に変更するとグラデーションを色を動的に変化させられます。
スライダーのノブを移動して実際に生成される色がグラデーションと一致するようになるので、分かりやすいスライダーになります。
ColorPickerModelの追加
ColorPickerChannelModel
クラスとColorPickerGradationModel
クラスを元にして、ColorPickerModel
クラスを作ります。
現在値のプロパティを定義する
import Foundation
import SwiftUI
class ColorPickerModel : ObservableObject {
/// 現在値: Red
@Published var red: Double = 0.0
/// 現在値: Green
@Published var green: Double = 0.0
/// 現在値: Blue
@Published var blue: Double = 0.0
}
現在値を元に色を取得する
各チャネルの現在値を元に色を取得するプロパティを追加します。
/// 選択色
var color: Color {
Color(red: red, green: green, blue: blue)
}
現在値の丸め込み処理付きの変更メソッド
現在値を設定するときに、丸め込み処理を行うメソッドを実装します。チャネル毎にメソッドを用意するしても良いのですが、他にもチャネル毎に処理する必要があるメソッドが出てくると思いますので、チャネルの指定を引数に取る形式で実装します。
/// チャネル
enum Channel {
case red
case green
case blue
}
/// 現在値を変更する
/// - Parameters:
/// - value: 新しい値。範囲外のときは丸め込みされる
/// - channel: 変更するチャネル
func changeCurrentValue(_ value: Double, channel: Channel) {
var newValue = value
if newValue < 0.0 {
newValue = 0.0
} else if newValue > 1.0 {
newValue = 1.0
}
switch channel {
case .red:
red = newValue
case .green:
green = newValue
case .blue:
blue = newValue
}
}
編集中のテキストとバインディング
編集中のテキストを代入するプロパティをチャネル毎に実装します。また、バインディングは動的に作成するので、チャネルを引数にとるメソッドを実装します。
/// 入力中のテキスト: Red
private var fieldTextStoreRed: String?
/// 入力中のテキスト: Green
private var fieldTextStoreGreen: String?
/// 入力中のテキスト: Blue
private var fieldTextStoreBlue: String?
/// 入力中のテキストのバインディング: Red
var fieldTextRed: Binding<String> {
Binding {
self.fieldTextStoreRed ?? String(format: "%g", self.red)
} set: {
self.fieldTextStoreRed = $0
if let value = Double($0) {
self.changeCurrentValue(value, channel: .red)
}
}
}
/// 入力中のテキストのバインディング: Green
var fieldTextGreen: Binding<String> {
Binding {
self.fieldTextStoreGreen ?? String(format: "%g", self.green)
} set: {
self.fieldTextStoreGreen = $0
if let value = Double($0) {
self.changeCurrentValue(value, channel: .green)
}
}
}
/// 入力中のテキストのバインディング: Blue
var fieldTextBlue: Binding<String> {
Binding {
self.fieldTextStoreBlue ?? String(format: "%g", self.blue)
} set: {
self.fieldTextStoreBlue = $0
if let value = Double($0) {
self.changeCurrentValue(value, channel: .blue)
}
}
}
/// 入力中のテキストをクリアする
/// - Parameters:
/// - channel: チャネル
func clearFieldText(channel: Channel) {
switch channel {
case .red:
fieldTextStoreRed = nil
case .green:
fieldTextStoreGreen = nil
case .blue:
fieldTextStoreBlue = nil
}
}
グラデーションビューのサイズ
グラデーションビューのサイズを入れるプロパティもチャネル毎に定義します。実際には同じサイズなので一つでも良いのですが。。。
/// グラデーションビューのサイズ: Red
@Published var gradationViewSizeRed: CGSize = CGSize()
/// グラデーションビューのサイズ: Green
@Published var gradationViewSizeGreen: CGSize = CGSize()
/// グラデーションビューのサイズ: Blue
@Published var gradationViewSizeBlue: CGSize = CGSize()
グラデーションの開始色と終了色
グラデーションの開始色と終了色を取得するプロパティを定義します。こちらもチャネル毎です。
/// グラデーションの開始色: Red
var startColorRed: Color {
Color(red: 0.0, green: self.green, blue: self.blue)
}
/// グラデーションの終了色: Red
var endColorRed: Color {
Color(red: 1.0, green: self.green, blue: self.blue)
}
/// グラデーションの開始色: Green
var startColorGreen: Color {
Color(red: self.red, green: 0.0, blue: self.blue)
}
/// グラデーションの終了色: Green
var endColorGreen: Color {
Color(red: self.red, green: 1.0, blue: self.blue)
}
/// グラデーションの開始色: Blue
var startColorBlue: Color {
Color(red: self.red, green: self.green, blue: 0.0)
}
/// グラデーションの終了色: Blue
var endColorBlue: Color {
Color(red: self.red, green: self.green, blue: 1.0)
}
PickerGradationViewの対応
ColorPickerChannelModel
とColorPickerGradationModel
を使っているところをColorPickerModel
に置き換えます。
モデルクラスの変更
PickerGradationView
から対応します。ColorPickerChannelModel
クラスとColorPickerGradationModel
クラスのインスタンスを代入するプロパティを削除します。代わりに、ColorPicker.Channel
とColorPickerModel
クラスのインスタンスを代入するプロパティを追加します。
struct PickerGradationView: View {
var channel: ColorPickerModel.Channel
@ObservedObject var model: ColorPickerModel
indicatorXプロパティの実装変更
indicatorX
プロパティはchannel
プロパティで指定されたチャネルの値を使って計算された値になるように変更します。
/// ノブのX座標
var indicatorX: CGFloat {
switch channel {
case .red:
return indicatorXRed
case .green:
return indicatorXGreen
case .blue:
return indicatorXBlue
}
}
var indicatorXRed: CGFloat {
model.gradationViewSizeRed.width * model.red - indicatorSize / 2
}
var indicatorXGreen: CGFloat {
model.gradationViewSizeGreen.width * model.green - indicatorSize / 2
}
var indicatorXBlue: CGFloat {
model.gradationViewSizeBlue.width * model.blue - indicatorSize / 2
}
dragプロパティの実装変更
drag
プロパティの実装を変更します。計算時のグラデーションビューの幅はchannel
プロパティで指定されたチャネル用のグラデーションビューの幅を使うようにします。また、changeCurrentValue
メソッドとclearFieldText
メソッドはチャネルを指定するようになったので、チャネルの指定を追加します。
var drag: some Gesture {
DragGesture()
.onChanged { dragValue in
let width: Double
switch channel {
case .red:
width = self.model.gradationViewSizeRed.width
case .green:
width = self.model.gradationViewSizeGreen.width
case .blue:
width = self.model.gradationViewSizeBlue.width
}
let value = dragValue.location.x / width
model.changeCurrentValue(value, channel: channel)
model.clearFieldText(channel: channel)
}
}
グラデーションの開始色と終了色の取得
グラデーションの開始色と終了色を取得するプロパティを追加します。channel
プロパティの値によって参照するプロパティを変更します。
var startColor: Color {
switch channel {
case .red:
return model.startColorRed
case .green:
return model.startColorGreen
case .blue:
return model.startColorBlue
}
}
var endColor: Color {
switch channel {
case .red:
return model.endColorRed
case .green:
return model.endColorGreen
case .blue:
return model.endColorBlue
}
}
追加したstartColor
プロパティとendColor
プロパティを使ってグラデーションを描くようにGradient
の引数を変更します。
var body: some View {
ZStack(alignment: .leading) {
GeometryReader() { geometry in
LinearGradient(gradient: Gradient(colors: [startColor, endColor]),
startPoint: UnitPoint(x: 0, y: 0),
endPoint: UnitPoint(x: 1, y: 0))
}
タップされたときの処理の変更
タップされたときに現在値の更新と入力中の文字列のクリアを行います。このときに変更する対象もchannel
プロパティによって変更するようにコードを変更します。
var body: some View {
ZStack(alignment: .leading) {
GeometryReader() { geometry in
LinearGradient(gradient: Gradient(colors: [startColor, endColor]),
startPoint: UnitPoint(x: 0, y: 0),
endPoint: UnitPoint(x: 1, y: 0))
}
.frame(height: gradationHeigh)
.background {
GeometryReader { geometry in
Path { path in
Task.detached {
await updateViewSize(geometry.size)
}
}
}
}
.onTapGesture { point in
switch self.channel {
case .red:
model.red = point.x / model.gradationViewSizeRed.width
case .green:
model.green = point.x / model.gradationViewSizeGreen.width
case .blue:
model.blue = point.x / model.gradationViewSizeBlue.width
}
model.clearFieldText(channel: channel)
}
.gesture(drag)
PickerIndicator()
.frame(width: indicatorSize, height: indicatorSize)
.offset(x: indicatorX)
}
}
updateViewSizeの変更
updateViewSize
メソッドで更新する対象をchannel
プロパティによって変更するようにコードを変更します。
@MainActor
func updateViewSize(_ size: CGSize) async {
switch channel {
case .red:
if size != model.gradationViewSizeRed {
model.gradationViewSizeRed = size
}
case .green:
if size != model.gradationViewSizeGreen {
model.gradationViewSizeGreen = size
}
case .blue:
if size != model.gradationViewSizeBlue {
model.gradationViewSizeBlue = size
}
}
}
プレビューの変更
ストアドプロパティが変わったので、Xcodeのライブプレビュー用のコードも変更します。
struct PickerGradationView_Previews: PreviewProvider {
static var previews: some View {
PickerGradationView(channel: .red,
model: ColorPickerModel())
.padding()
}
}
ColorPickerSubViewの対応
PickerGradationView
のストアドプロパティが変わったので、PickerGradationView
を使っているColorPickerSubView
もコード変更が必要です。
モデルプロパティの変更
ColorPickerChannelModel
クラスとColorPickerGradationModel
クラスを使ったプロパティを削除し、代わりにColorPickerModel.Channel
とColorPickerModel
クラスのインスタンスを代入するプロパティを追加します。
struct ColorPickerSubView: View {
var channel: ColorPickerModel.Channel
@ObservedObject var model: ColorPickerModel
チャネル名
チャネル名を上位から渡すコードになっていますが、チャネルが分かるので、channel
プロパティによって文字列を変更するように実装を変更します。
var channelName: String {
switch channel {
case .red:
return "Red"
case .green:
return "Green"
case .blue:
return "Blue"
}
}
var body: some View {
VStack {
HStack {
Text("\(channelName): ")
.font(.title)
テキストフィールドのバインディング
TextField
に渡すバインディングをchannel
プロパティによって変更するようにコードを変更します。
var fieldText: Binding<String> {
switch self.channel {
case .red:
return self.model.fieldTextRed
case .green:
return self.model.fieldTextGreen
case .blue:
return self.model.fieldTextBlue
}
}
var body: some View {
VStack {
HStack {
Text("\(channelName): ")
.font(.title)
TextField("", text: fieldText)
.textFieldStyle(.roundedBorder)
}
PickerGradationViewのイニシャライザ変更
PickerGradationView
のストアドプロパティが変わり、イニシャライザに渡す引数が変わっています。
var body: some View {
VStack {
HStack {
Text("\(channelName): ")
.font(.title)
TextField("", text: fieldText)
.textFieldStyle(.roundedBorder)
}
PickerGradationView(channel: channel, model: model)
}
}
ColorPickerSubView_Previewsの変更
ColorPickerSubView
のストアドプロパティが変わったので、ライブプレビュー用に生成するColorPickerSubView
のイニシャライザの引数を変更します。
struct ColorPickerSubView_Previews: PreviewProvider {
static var previews: some View {
ColorPickerSubView(channel: .red, model: ColorPickerModel())
.padding()
}
}
ColorPickerの対応
ColorPickerSubView
のストアドプロパティが変わったので、イニシャライザの引数の変更が必要です。また、モデルをColorPickerModel
に変更するようにコード変更が必要です。
モデルの変更
以下のプロパティが不要になったので、削除します。
redChannel
redGradation
greenChannel
greenGradation
blueChannel
blueGradation
代わりにColorPickerModel
クラスのインスタンスを代入するmodel
プロパティを追加します。
struct ColorPicker: View {
@StateObject var model: ColorPickerModel = ColorPickerModel()
colorプロパティの削除
ColorPicker.color
プロパティは不要になったので削除します。ColorPickerPreview
のイニシャライザに渡す色はColorPickerModel.color
プロパティを渡すように変更します。
ColorPickerPreview(color: model.color)
.frame(width: 100, height: 100)
ColorPickerPreview
はバインディングを渡していたのですが、プレビュー側で変更することはないので、バインディングではなく、値渡しに変更します。
import SwiftUI
struct ColorPickerPreview: View {
var color: Color
var body: some View {
GeometryReader { geometry in
Path { path in
path.addRect(CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height))
}
.fill(color)
Path { path in
path.addRect(CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height))
}
.stroke(lineWidth: 4)
}
}
}
struct ColorPickerPreview_Previews: PreviewProvider {
static var previews: some View {
ColorPickerPreview(color: .blue)
}
}
ColorPickerSubViewのイニシャライザ変更
ColorPickerSubView
のストアドプロパティが変わったので、イニシャライザの変更が必要です。
struct ColorPicker: View {
@StateObject var model: ColorPickerModel = ColorPickerModel()
var body: some View {
VStack {
ColorPickerSubView(channel: .red, model: model)
ColorPickerSubView(channel: .green, model: model)
ColorPickerSubView(channel: .blue, model: model)
VStack {
Text("Preview")
ColorPickerPreview(color: model.color)
.frame(width: 100, height: 100)
}
.padding()
}
.padding()
}
}
動作テスト
動作テストをしてみましょう。Xcodeのライブプレビューがコード変更直後はおかしくなりました。
おかしい場合は、プロジェクトをクリーンしたり、実機やiOSシミュレータで実行し、Xcodeを再起動すると直ると思います。
コードのダウンロード
今回の記事で作成したコードのダウンロードはこちらです。