はじめての Swift UI × watchOS 〜タイマーアプリを作る〜
せっかく Apple Watch を持っているので何かアプリを作りたいと思い Swift UI を勉強中です。 Swift UI の入門として作ったタイマーアプリについてチュートリアル形式でまとめます。
※ 本業は Web フロントエンドなので、Swift 自体が初めてです。間違い等あれば、気軽にコメントいただけると嬉しいです🙏
作るもの
簡単なタイマーアプリを作ります。
ピッカーで秒数を選んでStart
を押すとタイマーが起動して、設定された秒数が経過すると、通知音とともにタイマーが終了するという簡単なものです。
コードは全て以下リポジトリにあります。
1. Xcode プロジェクトのスタート
Xcode を開き Create a new Xcode project
で新しいプロジェクトを作成します。
最初の画面で何を作るのか選択できるので、Watch App を選択して Next を押します。
適当に Product Name などを埋めていきます。
これでプロジェクトの準備は完了です。
この状態でビルドすると、Apple Watch のプレビューに Hello World と表示されるはずです。
2. 秒数ピッカー画面の作成
準備が整ったので画面を作っていきます。
最初にタイマーの起動時間を設定するための秒数ピッカーを作ります。
contentView
を以下のように変更します。
struct ContentView: View {
@State var timeVal = 1
var body: some View {
VStack {
Text("Timer \(self.timeVal) seconds").font(.body)
Picker(selection: self.$timeVal, label: Text("")) {
Text("1").tag(1).font(.title2)
Text("5").tag(5).font(.title2)
Text("10").tag(10).font(.title2)
Text("30").tag(30).font(.title2)
Text("60").tag(60).font(.title2)
}
Button(action: {},label: {
Text("Start")
})
}
}
}
この状態で、プレビューを確認すると以下のようにタイマーの秒数選択画面が表示出来ているはずです。
簡単にコードの説明をします。
Swift UI では、Vue や React のように宣言的 UI でアプリの画面を構築していきます。
このコードで使ったコンポーネントは以下の通りです。
-
VStack
: 内部の要素を縦方向に並べる -
Text
: テキストを表示する -
Picker
: 内部の要素をピッカー形式で表示する -
Button
: ボタンを表示する
また、それぞれのコンポーネントに引数やクロージャーで、設定値や別のコンポーネントを与え階層構造を作って UI を組み立てます。
以下コードの場合は、Text というコンポーネントの Struct を()
内のテキストで初期化し、.font
でフォントサイズを設定しています。
とても直感的で分かりやすいですね。
Text("Timer \(timeVal) seconds").font(.body)
3. Timer画面への移動
次に Start ボタンを押した後に、タイマー画面への画面遷移をさせたいのでその部分を実装します。
まず新しい View を作ります。
@State
として受け取った値を表示するだけの画面です。
struct TimerView: View {
@Binding var timerScreenShow:Bool
@State var timeVal:Int
var body: some View {
Text("\(self.timeVal)")
}
}
次に最初の画面から、先ほど作った View に移動するためのコードを実装します。
struct ContentView: View {
@State var timeVal = 1
@State var timerScreenShow:Bool = false
var body: some View {
VStack {
Text("Timer \(self.timeVal) seconds").font(.body)
Picker(selection: self.$timeVal, label: Text("")) {
Text("1").tag(1).font(.title2)
Text("5").tag(5).font(.title2)
Text("10").tag(10).font(.title2)
Text("30").tag(30).font(.title2)
Text("60").tag(60).font(.title2)
}
NavigationLink(
destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: self.timeVal),
isActive: self.$timerScreenShow,
label: {
Text("Start")
})
}
}
}
ポイントは、Button
をNavigationLink
に変更して、ボタン押下で TimerView への遷移を実行している点です。
そのために@State
として、タイマー画面の表示状態を制御するtimerScreenShow
を宣言しています。
timerScreenShow
と、timeVal
はNavigationLink
にて、TimerView
を初期化する際に、引数として渡しています。
この状態でプレビューを起動すると以下のような画面になります。
ちゃんとTimerView
に遷移していますね。
4. Timer機能の完成
次に Timer 機能を、完成させます。
TimerView
を以下のように変更します。
struct TimerView: View {
@Binding var timerScreenShow:Bool
@State var timeVal:Int
var body: some View {
if timeVal > -1 {
VStack {
Text("\(self.timeVal)").font(.system(size: 40))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Cancel")
.foregroundColor(Color.red)
})
.padding(.top)
}
} else {
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.font(.title)
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
}
}
}
いきなりコードが増えたのですが、1 つづつ説明します。
最初の if 文の分岐ではContentView
から受け取ったtimeVal
の値によって View を切り分けています。
これは、タイマー終了時の画面を表示するためです。
if timeVal > -1 {
// ...
} else {
//...
}
次にタイマー部分のコードです。
タイマー部分では、timeVal
の値を表示しながら、onAppear()
(要素の表示にフックして処理を実行するもの)で、Timer.scheduledTimer
を実行して、1 秒ごとにtimeVal
の値をディクリメントしていきます。
Swift UI では、@State
や @Binding
のアノテーションをつけて宣言した変数が変化すると、View が再描画されるのでこれだけで、タイマーが実装できます。
Text("\(self.timeVal)").font(.system(size: 40))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
最後に、完了部分のコードです。
Button
コンポーネントにonAppear()
で、WKInterfaceDevice.current().play(.notification)
を実行しています。これは完了を音で知らせるための通知音の起動です。
また、action
としてself.timerScreenShow
にfalse
を代入しています。
これで「Done!」のボタンを押すと最初の画面に遷移する動きが実現できています。
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.font(.title)
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
この状態でプレビューを起動すると以下のような画面になります。
タイマー機能が完成ですね。
5. UIの改善
今のままでも、タイマーとして機能するのですが、ちょっとまだ画面がシンプルすぎますよね。
プラスアルファとして、Apple Watch でよくみる進捗バーを追加してみます。
最初にバー部分を別 View で実装します。
要素を重なりで配置するZStack
で、円を作るCircle
を 2 つ重ねて実装しています。
引数として受け取るのは、初期値のinitial
と、進捗のprogress
です。
struct ProgressBar: View {
let progress: Int
let initial: Int
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 15.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(Float(self.progress) / Float(self.initial), 1.0)))
.stroke(style: StrokeStyle(lineWidth: 15.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.red)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
}
}
}
これをTimerView
に組み込みます。
Timer の値をText
で表示している部分をZStack
で囲み、先ほどの ProgressBar を表示しています。
また、初期値設定用のinitialTime
というプロパティを新たに追加しています。
struct TimerView: View {
@Binding var timerScreenShow:Bool
@State var timeVal:Int
let initialTime:Int
var body: some View {
if timeVal > -1 {
VStack {
ZStack {
Text("\(self.timeVal)").font(.system(size: 40))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
ProgressBar(progress: self.timeVal, initial: self.initialTime).frame(width: 90.0, height: 90.0)
}
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Cancel")
.foregroundColor(Color.red)
})
.padding(.top)
}
} else {
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.font(.title)
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
}
}
}
initialTime
は、ContentView
でTimerView
を描画するときに渡します。
destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: self.timeVal, initialTime: self.timeVal),
この状態でプレビューをみると、タイマーの数値に合わせて良い感じにバーの状態も変わるはずです。
これでタイマーアプリの完成です 🎉
おわりに
以上「はじめての Swift UI × watchOS 〜タイマーアプリを作る〜」でした。
View の構築については、Vue, React で慣れ親しんだ宣言的 UI なので あまり抵抗なく実装出来そうです。ただ、他のビルドやメモリ処理、データの永続化、ネイティブ API の使い方はまだ何もわかりません😅
これから色々アプリを作っていく段階で勉強していきたいです。
Discussion