##記事
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、プログレスバーに自作のグラデーションカラーを適用し、時間経過とともに色相を変化させる方法について掲載します。
##環境
- OS: macOS 10.15.7 (Catalina)
- エディタ: Xcode 12.1
- 言語: Swift
- 主な使用ライブラリ: SwiftUI
##Gitリポジトリ
以下のGitリポジトリのURLからサンプルコードをご覧いただけます。
https://github.com/msnsk/Qiita_Timer.git
##手順
- プログレスバーをどういう色にするか検討する
- ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する
- ProgressBarView にグラデーションを生成するメソッドを作成する
- プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する
- ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる
###1. プログレスバーをどういう色にするか検討する
プログレスバーの色を美しく表現するにはどうすれば良いかを考えます。これは人によって好みが別れるかもしれません。私の場合、は以下の2点が重要だと考えました。
一つは、単色ではなくグラデーションで表示すること。
もう一つは、時間経過とともに色が変化すること。
この2つを実装して、アプリのビジュアル的な印象を向上させます。
###2. ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する
グラデーションを作るには最低 2 色が必要です。この2色を自作していきます。
Swift では、色の指定方法はいくつかあります。それは一般的な色の規格とも合致しています。例えば、RGB、CMYK、HSB などです。
簡単に説明すると、RGB は Red、Green、Blue の光の三原色をそれぞれどの程度配合するかで色を表現するものです。3 色全て 100% なら白、 0% なら黒になります。
CMYK は色の三原色 + K:Black で、RGB とは逆に 3 色全て 100% なら黒、0% なら白になります。インクジェットプリンタを想像するとわかりやすいかと思います。
そして、HSB は、色相(Hue)、彩度(Saturation)、明度(Brightness)で色を表します。Hue は何色かを示し、Saturation は色味がどの程度か(0ならモノトーン)、Brightness は白の割合で 100% なら白なので色が濁りません。
最終的に色を常に変化させることを考えると、HSB ならば、Hue の値のみを変化させ、Saturation と Brightness を固定しておけば調整しやすそうですので、ここでは HSB での色指定を採用します。
それでは ProgressBarView に色相(Hue)を格納するプロパティを2色分追加します。それぞれ customHueA、customHueB とします。
HSB での色指定の場合、Color() は以下のような引数が求められます。それぞれデータ型は CGFloat 型で、最小値が 0.0、最大値が 1.0 と定められています。
Color(hue:, saturation:, brightness:)
そのため、新規に追加する Hue のプロパティの値の初期値として、ひとまず 0.5 と 0.3 をそれぞれ代入しておきます。0.5 が青緑色、 0.3 が黄緑色で、比較的近い色味にしておきます。
例えば 0.0 と 0.5 だと色相が真逆になって最終的にグラデーションにしたときに中間が濁った色になりがちです。そのため、ここでは色相の差を 0.2 にとどめておきました。ちなみに Hue 0.0 と Hue 1.0 では一周回って同じ色になります。
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
@State var costomHueA = 0.5
@State var costomHueB = 0.3
var body: some View {
//(省略)
}
}
###3. ProgressBarView にグラデーションを生成するメソッドを作成する
次に、Hue の値を格納した2つのプロパティから、円形プログレスバーにふさわしいグラデーションを作成するメソッドを作成していきます。
メソッドの名前は makeGradientColor としました。
引数は Double 型のデータをとる引数 hueA と hueB です。ここに先に作成したプロパティを入れる予定です。
そして、戻り値のデータ型は、円すい状グラデーションの AngularGradient です。ちなみに、Swift のグラデーションは他にも、線形グラデーション(LinearGradient)や放射状グラデーション(RadialGradient)があります。円をぐるりと一周する方向でグラデーションがかかるのが AngularGradient です。
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {}
次に makeGradientColor メソッドの {} の中を記述していきます。まず、メソッドの {} 内の最初にグラデーションに必要な2色を作るプロパティを用意します。
ColorA は引数 hueA の値から色を作ります。ここには ProgressBarView のプロパティ costomHueA が入る予定です。同様に、ColorB は引数 hueB の値から色を作ります。こちらには costomHueB が入る予定です。
ColorA も ColorB も saturation と brightness は同じ数値を入れます。再度はやや高め、明度はかなり高めにしています。
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
}
そして最後に、作った2色から円錐型グラデーションを作ります。AngularGradient 構造体のインスタンスを作成するわけですが、引数 gradient には イニシャライザ .init() を入れ、さらにその引数には Array でグラデーションの開始から終了までの色を入れます。
ここで以下のようにすると、円の始点と終端の重なるところで、くっきり色が分かれてしまいます。
[colorA, colorB]
ですので、以下のように、colorB からまた colorA へグラデーションをかけるようにします。
[colorA, colorB, colorA]
もっと細かく中間にも色を置いて調整することもできます。例えば以下のような具合です。ここでは2色でのグラデーションにとどめておきます。
[colorA, colorB, colorC, colorD, colorA]
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}
メソッドは body{} の下に記述します。
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
@State var costomHueA = 1.0
@State var costomHueB = 0.5
var body: some View {
//(省略)
}
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}
}
###4. プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する
これまでプログレスバーの円の色は .stroke モディファイアの引数で単色のシアンを指定していました。
.stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
ここで、さきほど作ったメソッドを Color(.cyan) の代わりに引数として入れます。メソッドの引数 hueA と hueB には先に用意していたプロパティ costomHueA と costomHueB をそれぞれ入れます。
.stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
コード全体としては以下のようになります。
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
@State var costomHueA = 1.0
@State var costomHueB = 0.5
var body: some View {
ZStack {
//(背景用の円省略)
//プログレスバー用の円
Circle()
.trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
//メソッドで生成したグラデーションカラーを適用
.stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.scaledToFit()
//輪郭線の開始位置を12時の方向にする
.rotationEffect(Angle(degrees: -90))
.padding(15)
}
}
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}
}
###5. ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる
手順4で、プログレスバーの色をプロパティの色相から指定する形でグラデーション表示にすることができました。
さらに、onReceive モディファイアで、TimeManager クラスの timer プロパティ(Timer.publish() メソッド)の発動をトリガーにして、毎 0.05 秒ごとに色相のプロパティ costomHueA と costomHueB の値を変化させて、プログレスバーの色が常に変化するようにしていきます。
0.05 秒ごとに色相の値を 0.005 ずつ足していきます。もっとゆったり色を変化させたい場合は 0.001、もっとコロコロ色が変わる方が良ければ 0.01 くらいが良いかと思います。これ以上数値を上げると、色がただチカチカして目が痛くなりそうな動きになってしまいます。
また、色相は1.0が最大値ですので、1.0になったら、0.0に戻すように if 文で記述します。
.onReceive(timeManager.timer) { _ in
self.costomHueA += 0.005
if self.costomHueA >= 1.0 {
self.costomHueA = 0.0
}
これを costomHueB の分も同様に記述します。コード全体では以下のようになります。
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
@State var costomHueA = 0.5
@State var costomHueB = 0.3
var body: some View {
ZStack {
//背景用の円
Circle()
.stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
.scaledToFit()
.padding(15)
//プログレスバー用の円
Circle()
.trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
//メソッドで生成したグラデーションカラーを適用
.stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.scaledToFit()
//輪郭線の開始位置を12時の方向にする
.rotationEffect(Angle(degrees: -90))
.padding(15)
}
//毎0.05秒ごとに発動
.onReceive(timeManager.timer) { _ in
self.costomHueA += 0.005
if self.costomHueA >= 1.0 {
self.costomHueA = 0.0
}
self.costomHueB += 0.005
if self.costomHueB >= 1.0 {
self.costomHueB = 0.0
}
}
}
func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}
}
これで美しいグラデーションで、かつ常に色相が変化する円形プログレスバーができました。