作成中です。
Part6では、以下のようなサイコロアプリを開発します。ボタンを押したらサイコロの目がランダムで表示されます。
Contents
プロジェクト作成
まずはプロジェクトを作成しましょう。
Xcodeを立ち上げて、Create a new Xcode projectをクリック。
この画面が開かない場合は、command + shift + 1を押してください。
①iOSを選択
②Appを選択
③Nextをクリック
①Product Nameに、Dice
②Interfaceは、SwiftUI
③Languageは、Swift
④全てチェックなし
⑤Nextをクリック
これでプロジェクトが作成できました。
レイアウト実装
まずは、見た目の部分レイアウトを実装していきましょう。
まずは、Resumeを押して画面を確認してください。そうすると、Hello, world!の画面が表示されると思います。この講座の最中は、こまめにResumeを押して画面を確認してみてください。
Text("Hello, world!")
の部分を以下のように書き換えましょう。
struct ContentView: View { var body: some View { VStack { Image(systemName: "die.face.1") Button(action: { print("ボタンが押されたよ") }) { Text("サイコロを振る") } } } }
Resumeを押すと、このようなレイアウトになっているはずです。
縦に並べたいので、VStackを使います。
VStackの中には、2つのオブジェクトがあります。
1. 画像
Image(systemName: "die.face.1")
Image(systemName: "画像の名前")
で、Xcodeにあらかじめ登録されている画像を表示できます。
登録されている画像の一覧は、SF Symbolsというアプリをダウンロードすると一覧がみれます。SwiftUI開発では必須のアプリなのでインストしておきましょう。
2. ボタン
Button(action: { print("ボタンが押されたよ") }) { Text("サイコロを振る") }
前回配置したボタンとちょっと違う書き方ですが、これでボタンが配置できます。
action: {
の中に、ボタンを押されたときの処理を書いて、その下の}) {
の中に、ボタンのレイアウトを書きます。
画像が小さすぎるので大きくしましょう。
Image(systemName: "die.face.1") .resizable() .scaledToFit() .frame(width: UIScreen.main.bounds.width/2) .padding()
Image()
に対して、.~~~
というふうに書くと、そのオブジェクトに装飾ができます。これをモディファイアと言います。なので、今回は、Imageに4つのモディファイアをつけてるみたいなイメージです。
今回つけたモディファイアを一つ一つ解説します。
.resizable()
画像の大きさを変更する時は必ずこのモディファイアをつけないといけません。
.scaledToFit()
画像の縦横の比率を固定するというモディファイアです。これを指定しないと、画像が伸びちゃったりぺちゃんこになったりします。
.frame(width: UIScreen.main.bounds.width/2)
.frame
は、オブジェクトの大きさを変更できるモディファイアです。画像の比率を固定するという指定をしているので、ここでは幅だけ指定しています。
UIScreen.main.bounds.width
で、画面の幅が取得できます。それを/2
しているので、画面の幅の半分の大きさに指定するという意味になります。
.padding()
.padding()
は、内側の余白です。HTML/CSSに触れたことがある方はわかりやすいと思います。.padding(30)
というふうに数値で余白の大きさを変えることができます。デフォルトは16です。
次はボタンを装飾しましょう。
Text("サイコロを振る") .padding() .background(.orange) .foregroundColor(.black) .cornerRadius(10)
ビルドするとこのようなレイアウトになっているはずです。
こちらもモディファイアを一つ一つ解説していきます。
.padding()
内側の余白です。
.background(.orange)
背景色を変更できます。実は、.background(Color.orange)
の.Color
を省略している形になっています。
.foregroundColor(.black)
文字色を変更できます。ボタンの中の文字は自動的に青色になるので、ここで黒文字に変更しています。
.cornerRadius(10)
角に丸みをつけることができます。ボタンには丸みをつけると大体いい感じになります。
現時点では、真ん中に寄りすぎているので、もう少し画像とボタンのスペースを広げてあげます。
そういう時は、Spacer()
を追記してバランスを取ります。
Imageの上と下、ボタンの下にSpacer()を追記しましょう。
VStack { Spacer() Image(systemName: "die.face.1") .resizable() .scaledToFit() .frame(width: UIScreen.main.bounds.width/2) .padding() Spacer() Button(action: { print("ボタンが押されたよ") }) { Text("サイコロを振る") .padding() .background(Color.orange) .foregroundColor(.black) .cornerRadius(10) } Spacer() }
Spacer()
は、限界までスペースを作ることができます。複数あるとスペースの幅は均等になります。
SwiftUIはこのような感じでSpacer()
を使ってレイアウトを整えていきます。
これでレイアウトがいい感じになりました。
機能実装
では、いよいよサイコロを振る機能を作っていきましょう。
例えば、サイコロの出目を3に変更するには、"die.face.1"
を"die.face.3"
というふうに変更します。
試しに、変更して実行してみましょう。
Image(systemName: "die.face.3")
このように出目が3に変わっているはずです。
そのため、"die.face.1"
の数値を変えることで、出目を変更できるということがわかりました。
"die.face.1"
の数値を変数で持つようにしましょう。
@State var randomNumber = 1
では、先ほど作った変数を、画像の数値に対応させます。
Image
を以下のように記載してください。
ちなみにバックスラッシュ(\)は、option + ¥で打てます。
Image(systemName: "die.face.\(randomNumber)")
\()
で囲うと文字列の中に変数を入れることができます。
つまり、randomNumber
が1なら、"die.face.1"
。randomNumber
が3なら、"die.face.3"
というふうになります。現状randomNumber
が1なので、出目は1になるはずです。
では、ボタンを押したら出目を4に変更してみましょう。
Button(action: { print("ボタンが押されたよ") randomNumber = 4 }) {
実行してボタンを押してみると、出目が1から4になるはずです。
今は、ボタンを押すと強制的に4になりますが、これをランダムに変更してみましょう。
randomNumber = Int.random(in: 1...6)
これで、ボタンを押すたびにサイコロの出目が変わるはずです。
これで最低限のサイコロアプリが完成しました!
アニメーション実装
現時点では、ボタンを押したら一瞬で出目が変わるので、前回と同じ出目だった場合、ボタンを押せたのかどうかがわかりにくいです。そのため、ボタンを押したら、サイコロを振ってる感を出しましょう。
では、どのようにするかというと、適当な出目を連続で0.5秒間、高速で表示させます。
どのように処理で実装するのかというと、ボタンを押したら、出目を0.1秒ごとにランダムに変えていきます。そして、0.5秒経ったら止めます。これで良い感じのアニメーションっぽくなります。
では実装してきましょう。
0.1秒ごとに処理をするには、Timerを使います。
まず、timerという変数を追加します。
@State var timer: Timer?
randomNumberにランダムな値を入れているところを以下のように変更しましょう。
Button(action: { print("ボタンが押されたよ") timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } }) {
これで、実行してボタンを押してみてください。高速で0.1秒ごとに出目が変わり続けると思います。
このブロックの中に処理を書くと、0.1秒ごとに実行されます。
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in }
ちなみに、withTimeInterval
を0.2
とかにすると、切り替わりが少し遅くなります。
では、次に、ボタンを押してから0.5秒後に出目を止めるようにしましょう。
Button(action: { print("ボタンが押されたよ") timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { timer?.invalidate() timer = nil } }) {
遅らせて処理を行いたい時には、DispatchQueue
を使います。
以下のコードの中に書くと、0.5秒後に実行されます。
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { }
そして、この2行でtimerをストップさせています。
timer?.invalidate() timer = nil
これで、アニメーションが完成しました!良い感じに動いています!
バグ修正
よくできていると思いきや、バグを発見しました。
2回連続タップすると、一生止まらなくなってしまいます。
簡単に理由を説明すると、1回目押したタイマーが止まる前に、2回目のタイマーが起動するため、1回目のタイマーを止めるタイミングを失ってしまったからです。
では、どのように対応するのかというと、2回連続タップさせないようにしましょう。(押すたびにタイマー止めるのもあり)
2回連続タップさせないようにするには、1回目押されてから0.5秒間は、ボタンを無効にするという処理を入れないといけません。
ボタンを無効にするには、以下のように書きます。とりあえず、無効にして実行して確認しましょう。
Button(action: { //省略 }) { Text("サイコロを振る") //省略 } .disabled(true)
実行してボタンを押してみるとボタンが反応しないはずです。
つまり、.disabled(true)
は、ボタンを無効にさせるかどうかのモディファイアです。
true
にすると無効になり、
false
にすると有効になります。
ということで、今回の対応は、ボタンを押してから0.5秒間は、true
で、それ以外はfalse
という変数を用意すれば良いということになります。
ボタンを押してから0.5秒間は、true
で、それ以外はfalse
という変数を用意します。
以下のコードを追記しましょう。
@State var isRolling = false
ボタンを押してから、0.5秒間をtrue
にするので、以下のようにコードを追記しましょう。
Button(action: { print("ボタンが押されたよ") isRolling = true timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { timer?.invalidate() timer = nil isRolling = false } }) {
ボタンを押した瞬間に、isRolling
をtrue
にして、0.5秒後にisRolling
をfalse
にしているだけです。
isRolling
を、.disabled
に反映させましょう。
Button(action: { //省略 }) { Text("サイコロを振る") //省略 } .disabled(isRolling)
実行して確認してみましょう。
おそらくボタンを2回連続タップしても永遠と止まらなくなることはないかと思います。
これで、サイコロアプリは完成です!
import SwiftUI struct ContentView: View { @State var randomNumber = 1 @State var timer: Timer? @State var isRolling = false var body: some View { VStack { Spacer() Image(systemName: "die.face.\(randomNumber)") .resizable() .scaledToFit() .frame(width: UIScreen.main.bounds.width/2) .padding() Spacer() Button(action: { print("ボタンが押されたよ") isRolling = true timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { timer?.invalidate() timer = nil isRolling = false } }) { Text("サイコロを振る") .padding() .background(.orange) .foregroundColor(.black) .cornerRadius(10) } .disabled(isRolling) Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
リファクタリング(余裕があったら)
リファクタリングというのは、アプリの動作や見た目を変えずに、コードを見やすく整理したり、保守性を高めたりすることです。
では、今回のコードをリファクタリングしていきましょう。
処理を関数化する
基本的に、var body: some View {
の中には処理を書かないようにしましょう。
var body: some View {
のブロックの下に、以下のコードを追記しましょう。
func playDice() { }
Button(action: {
の中に書いてある処理を全て関数の中に移動させましょう。
func playDice() { print("ボタンが押されたよ") isRolling = true timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { timer?.invalidate() timer = nil isRolling = false } }
Button(action: {
の中で先ほど作った関数を呼びましょう。
Button(action: { playDice() }) {
これで、処理を分けることができました。
リファクタリングしたら、必ず実行して挙動が変わっていないかを確認しましょう。
これで、処理を関数化することができました。
privateをつける
そのstruct
内でしか使わない、変数や定数、関数には、private
をつけましょう。
private
をつけると、他のstruct
から参照できなくなります。だから何?と思われるかもしれません。正直、理由は「念のため」です。例えば、家に誰も来ないってわかってても、家の鍵は閉めますよね。それと同じです。一応鍵をかけておくって感じです。
以下のように、変数と関数にprivateをつけましょう。
変数
@State private var randomNumber = 1 @State private var timer: Timer? @State private var isRolling = false
関数
private func playDice() {
これで、リファクタリング完了です!綺麗なコードでかけました!
import SwiftUI struct ContentView: View { @State private var randomNumber = 1 @State private var timer: Timer? @State private var isRolling = false var body: some View { VStack { Spacer() Image(systemName: "die.face.\(randomNumber)") .resizable() .scaledToFit() .frame(width: UIScreen.main.bounds.width/2) .padding() Spacer() Button(action: { playDice() }) { Text("サイコロを振る") .padding() .background(.orange) .foregroundColor(.black) .cornerRadius(10) } .disabled(isRolling) Spacer() } } private func playDice() { print("ボタンが押されたよ") isRolling = true timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in randomNumber = Int.random(in: 1...6) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { timer?.invalidate() timer = nil isRolling = false } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }