##内容
タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、タイマーにカウントダウンする最大時間を設定するViewの作成について掲載します。
##環境
- OS: macOS 10.15.7 (Catalina)
- エディタ: Xcode 12.1
- 言語: Swift
- 主な使用ライブラリ: SwiftUI
##Gitリポジトリ
以下のGitリポジトリのURLからサンプルコードをご覧いただけます。
https://github.com/msnsk/Qiita_Timer.git
##手順
- TimeManager クラスを作成
- TimeManager にカウントダウンの時間を格納するためのプロパティを作成
- PickerView を作成
- PickerView に Picker を配置する
- TimeManager に設定した時間を反映するためのメソッドを作成
- TimeManager にカウントダウン中の残り時間を表示するメソッドを作成
###1. TimeManager クラスを作成
TimeManager.swift というファイルを作成し、同名のClassを作成します。
のちのちのことを想定して ObservableObject というプロトコルを適用します。ObservableObject
とは、簡単に言うと、値が変化する変数のプロパティを監視し続けるために必要なプロトコルです。このプロトコルを適用したクラスに @Published というキーワード(Property Wrapper)を頭につけたプロパティ(var)を用意すると、その値を他の View から常に参照することができます。
import SwiftUI
class TimeManager: ObservableObject {
}
###2. TimeManager にカウントダウンの時間を格納するためのプロパティを作成
カウントダウン時間の設定として、以下の5つのプロパティを用意します。
- 時間単位の設定値を格納するプロパティ
- 分単位の設定値を格納するプロパティ
- 秒単位の設定値を格納するプロパティ
- 上記3つを合計したカウントダウン開始前の最大時間のプロパティ
- 合計値を最大値としてカウントダウン中に値が減少し続けるプロパティ
各プロパティの値は他のViewから常に参照できるようにしたいので、@Published プロパティラッパーをvarの前につけておきます。
時間、分、秒は Picker から Int 型で取得するため Int をデータ型に指定しておきます。一方、残り時間と最大時間は Double を指定します。これはあとでこれらのプロパティの値を利用しやすくするためです。プログラミングで変数を使った計算をさせるときは、その変数のデータ型を統一しないとエラーになります。
それぞれのプロパティにはデフォルトの値を入れておく必要があるため、0 にしておきます。
class TimeManager: ObservableObject {
//Pickerで設定した"時間"を格納する変数
@Published var hourSelection: Int = 0
//Pickerで設定した"分"を格納する変数
@Published var minSelection: Int = 0
//Pickerで設定した"秒"を格納する変数
@Published var secSelection: Int = 0
//カウントダウン残り時間
@Published var duration: Double = 0
//カウントダウン開始前の最大時間
@Published var maxValue: Double = 0
}
さらに、設定したカウントダウン時間の長さによって時間表示の形式を変えたい(例えば2時間30分に設定したら "02:30:00"、5分に設定したら "05:00"、30秒に設定したら "30" という表示にしたい)ので、enum を作ります。
Data.swiftというファイルを新たに作成し、TimeFormat という enum を作成します。
import SwiftUI
enum TimeFormat {
case hr
case min
case sec
}
そして、TimeManagerのクラスに作成した TimeFormat の enum をタイプに指定したプロパティを用意し、表示形式を指定できるようにします。デフォルトの値はひとまず min にしておきます。
class TimeManager: ObservableObject {
//(先に作成したプロパティ省略)
//設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
@Published var displayedTimeFormat: TimeFormat = .min
}
###3. PickerView を作成
PickerView.swift という新しいファイルを作成し、同名の View タイプの Struct (構造体)を作成します。
import SwiftUI
struct PickerView: View {
var body: some View {
}
}
まずは必要なプロパティを作成します。
ここで、先に作成した TimeManager クラスのインスタンスを作成しておきます。
@EnvironmentObject のキーワード(Property Wrapper)を先頭につけることで、TimeManagerの変化する変数の値を常に参照することができます。つまり、ObservableObject プロトコル適用のクラス内の @Published プロパティと View プロトコル適用の Struct 内の @EnvironmentObject プロパティは対になり常に同期されます。
struct PickerView: View {
//TimeManagerのインスタンスを作成
@EnvironmentObject var timeManager: TimeManager
//デバイスのスクリーンの幅
let screenWidth = UIScreen.main.bounds.width
//デバイスのスクリーンの高さ
let screenHeight = UIScreen.main.bounds.height
//設定可能な時間単位の数値:0から23までの整数のArray
var hours = [Int](0..<24)
//設定可能な分単位の数値:0から59までの整数のArray
var minutes = [Int](0..<60)
//設定可能な秒単位の数値:0から59までの整数のArray
var seconds = [Int](0..<60)
struct PickerView: View {
var body: some View {
}
}
###4. PickerView に Picker を配置する
PickerView の中に、以下のコンポーネントを HStack を使って左から順番に水平方向に並べます。
- 時間のPicker
- 時間単位のtext
- 分のPicker
- 分単位のtext
- 秒のPicker
- 秒単位のtext
時間 Picker の引数 selection には先に作ったインスタンス timeManager のプロパティ hourSelection を指定します。これにより、Picker で選んだ値が TimeManager クラスの同名プロパティへ代入されるようになります。
Picker の{}内では、ForEach で 0 から 23 それぞれに対して Picker に数値を表示するようにし、tag モディファイアによって実際に Picker で時間を選択した時に取得する値(データ型含む)を指定します。取得する値がInt 型、反映先の TimeManager のプロパティ hourSelection も Int 型で合わせています。
時間 Picker ができたら、Text コンポーネントで "hour" という単位を表示するよう指定します。
同様にして、分、秒の Picker と Text も作成します。
struct PickerView: View {
//(プロパティ部分の記述省略)
var body: some View {
//時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに
HStack {
//時間単位のPicker
Picker(selection: self.$timeManager.hourSelection, label: Text("hour")) {
ForEach(0 ..< self.hours.count) { index in
Text("\(self.hours[index])")
.tag(index)
}
}
//上下に回転するホイールスタイルを指定
.pickerStyle(WheelPickerStyle())
//ピッカーの幅をスクリーンサイズ x 0.1、高さをスクリーンサイズ x 0.4で指定
.frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4)
//上のframeでクリップし、フレームからはみ出す部分は非表示にする
.clipped()
//時間単位を表すテキスト
Text("hour")
.font(.headline)
//分単位のPicker
Picker(selection: self.$timeManager.minSelection, label: Text("minute")) {
ForEach(0 ..< self.minutes.count) { index in
Text("\(self.minutes[index])")
.tag(index)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: self.screenWidth * 0.1, height: self.screenWidth * 0.4)
.clipped()
//分単位を表すテキスト
Text("min")
.font(.headline)
//秒単位のPicker
Picker(selection: self.$timeManager.secSelection, label: Text("second")) {
ForEach(0 ..< self.seconds.count) { index in
Text("\(self.seconds[index])")
.tag(index)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:self.screenWidth * 0.1, height: self.screenWidth * 0.4)
.clipped()
//秒単位を表すテキスト
Text("sec")
.font(.headline)
}
}
}
###5. TimeManager に設定した時間を反映するためのメソッドを作成
TimeManager クラス内に setTimer というメソッドを作成します。
PickerView から TimeManager の時間・分・秒のプロパティの値を取得できるようになっているので、それらの値を秒に換算して合計し、残り時間および最大時間のプロパティに反映します。併せて、時間表示形式も合計時間によって条件分岐するようにします。
class TimeManager: ObservableObject {
//Pickerで設定した"時間"を格納する変数
@Published var hourSelection: Int = 0
//Pickerで設定した"分"を格納する変数
@Published var minSelection: Int = 0
//Pickerで設定した"秒"を格納する変数
@Published var secSelection: Int = 0
//カウントダウン残り時間
@Published var duration: Double = 0
//カウントダウン開始前の最大時間
@Published var maxValue: Double = 0
//設定した時間が1時間以上、1時間未満1分以上、1分未満1秒以上によって変わる時間表示形式
@Published var displayedTimeFormat: TimeFormat = .min
//Pickerで取得した値からカウントダウン残り時間とカウントダウン開始前の最大時間を計算し、
//その値によって時間表示形式も指定する
func setTimer() {
//残り時間をPickerから取得した時間・分・秒の値をすべて秒換算して合計して求める
duration = Double(hourSelection * 3600 + minSelection * 60 + secSelection)
//Pickerで時間を設定した時点=カウントダウン開始前のため、残り時間=最大時間とする
maxValue = duration
//時間表示形式を残り時間(最大時間)から指定する
//60秒未満なら00形式、60秒以上3600秒未満なら00:00形式、3600秒以上なら00:00:00形式
if duration < 60 {
displayedTimeFormat = .sec
} else if duration < 3600 {
displayedTimeFormat = .min
} else {
displayedTimeFormat = .hr
}
}
}
さらに、上記 setTimer メソッドを、PickerView で時間を設定したタイミングで発動できるように、PickerView のほうにボタンを追加します。最大時間=残り時間という式が入っているため、カウントダウン前つまり時間設定操作の直後が理想的だからです。
ボタンのアイコンは Apple 純正の SF Symbols から "checkmark.circle.fill" を使います。
.offset モディファイアでデフォルトの中央配置からやや下にずらして配置し、PickerView と重ならないように調整しています。
.opacity モディファイアで、時間、分、秒、いずれも設定が0の場合は透明度を 10%(x 0.1)にして、今はタップできませんというのを視覚的に表現しています。
モディファイアの引数を入力する際、if と else を使う代わりに「△△ ? □□ : ○○」 と記載することで「もし △△ だったら □□ そうでなければ ○○」という意味に完結に条件分岐する引数を与えられて便利です。また「&&」は「かつ」という意味で複数の条件に当てはまる場合を表現する際に使います。
.onTapGesture モディファイアの {} 内で、時間、分、秒いずれかのプロパティの値が0でなければ、タップしたときに setTimer メソッドが発動するようにします。if 文内の「||」は「または」の意味です。
struct PickerView: View {
//TimeManagerのインスタンスを作成
@EnvironmentObject var timeManager: TimeManager
//デバイスのスクリーンの幅
let screenWidth = UIScreen.main.bounds.width
//デバイスのスクリーンの高さ
let screenHeight = UIScreen.main.bounds.height
//設定可能な時間単位の数値
var hours = [Int](0..<24)
//設定可能な分単位の数値
var minutes = [Int](0..<60)
//設定可能な秒単位の数値
var seconds = [Int](0..<60)
var body: some View {
//ZStackでPickerとレイヤーで重なるようにボタンを配置
ZStack{
//時間、分、秒のPickerとそれぞれの単位を示すテキストをHStackで横並びに
HStack {
//(Picker部分省略)
}
//タップして設定を確定するチェックマークアイコン
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
.offset(y: self.screenWidth * 0.32)
.opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1)
.onTapGesture {
if self.timeManager.hourSelection != 0 || self.timeManager.minSelection != 0 || self.timeManager.secSelection != 0 {
self.timeManager.setTimer()
}
}
}
}
}
###6. TimeManager にカウントダウン中の残り時間を表示するメソッドを作成
displayTimer という名前のメソッドにします。String 型の戻り値を返します。
残り時間の計算は算数ですが以下のとおりです。
ちなみに、%は割り算の余りを計算できる演算子です。
- 合計残り時間(秒) / 3600(秒) = 残り時間(時間)
- 合計残り時間(秒) % 3600(秒)/60(秒) = 残り時間(分)
- 合計残り時間(秒) / 3600(秒) % 60(秒) = 残り時間(秒)
上記計算で得られた3つの数値を、文字列型データとして画面に横並びに表示します。
String(format: "%02d:%02d:%02d", hr, min, sec)
このコードは、String(format: “桁数指定”, 値) という表記です。特に "" 内の %02d は全桁数が2桁で、2桁に満たない場合は大きい桁から0で埋めるという意味になります。そして %02d の箇所にその後ろに記載した値がカンマ区切りで左から順番に入る形になっています。
例えば、以下のように画面に表示されます。
- 残り時間が4000秒だったら 01:06:40
- 残り時間が350秒だったら 05:50
- 残り時間が7秒だったら、07
class TimeManager: ObservableObject {
//(省略)
//カウントダウン中の残り時間を表示するためのメソッド
func displayTimer() -> String {
//残り時間(時間単位)= 残り合計時間(秒)/3600秒
let hr = Int(duration) / 3600
//残り時間(分単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒
let min = Int(duration) % 3600 / 60
//残り時間(秒単位)= 残り合計時間(秒)/ 3600秒 で割った余り / 60秒 で割った余り
let sec = Int(duration) % 3600 % 60
//setTimerメソッドの結果によって時間表示形式を条件分岐し、上の3つの定数を組み合わせて反映
switch displayedTimeFormat {
case .hr:
return String(format: "%02d:%02d:%02d", hr, min, sec)
case .min:
return String(format: "%02d:%02d", min, sec)
case .sec:
return String(format: "%02d", sec)
}
}
}
ここまでで作成した PickerView は以下の画像のようになっているはずです。
次回は、Picker で設定した時間から残り時間を再計算して、画面にタイマーを表示させます。