Chapter 07無料公開

【Part6】サイコロアプリを作ってみよう!~ボタン・タイマー機能を利用~

Rikuto Sato
Rikuto Sato
2022.11.06に更新


このPartでは、以下のようなサイコロアプリを開発します。
ボタンを押したらサイコロの出目がランダムに表示されるだけのアプリです。

プロジェクト作成

まずはプロジェクトを作成しましょう。

  1. 新規作成

    Create a new Xcode projectを選択してください。
    もしこの画面が開かない場合は、command + shift + 1を押してください。

  2. テンプレートを選択

    ①iOSを選択
    ②Appを選択
    ③Nextをクリック

  3. プロジェクトの設定

    ①プロジェクト名にDiceと入力
    ②InterfaceはSwiftUI
    ③LanguageはSwift
    ④全てチェックなし
    ⑤Nextをクリック

  4. 保存するフォルダを選択

    上のところがPracticeフォルダになっていることを確認してCreateをクリック

これで、プロジェクトの作成ができました。

レイアウト実装

次に、レイアウトを実装していきましょう。

  1. ImageとButtonを配置
    今回のアプリは二つのオブジェクトしか使いません。
    以下のようにVStackの中を変更してください。

    VStack {
        Image(systemName: "die.face.1")
        Button {
    	print("ボタンが押されたよ")
        } label: {
    	Text("サイコロを振る")
        }
    }
    
    追記する場所

    現状、以下のようなレイアウトになっていればOKです。


  2. 画像の装飾
    次に、画像の装飾をしていきます。現状小さすぎるので、大きくします。
    Imageを以下のようにモディファイアをつけてください。

    Image(systemName: "die.face.1")
        .resizable()
        .scaledToFit()
        .frame(width: UIScreen.main.bounds.width/2)
        .padding()
    
    追記する場所


  3. ボタンの装飾
    次に、ボタンを装飾していきます。
    Textに以下のようにモディファイアをつけてください。

    Text("サイコロを振る")
        .padding()
        .background(.orange)
        .foregroundColor(.black)
        .cornerRadius(10)
    
    追記する場所

ここまで行うと以下のようなレイアウトになります。

  1. 間隔を変更する
    現時点では、真ん中に寄り過ぎているので、もう少し画像とボタンの間のスペースを広げてあげましょう。
    そういうときは、Spacer()を使います。
    Imageの上下、ボタンの下にSpacer()を追記してください。
    Spacer()
    
    追記する場所

これでレイアウトがいい感じになりました。

機能実装

では、いよいよサイコロを振る機能を実装していきましょう。

  1. サイコロを変更してみる
    例えば、サイコロの出目を3に変更するには、"die.face.1""die.face.3"というふうに変更します。
    試しに、変更して実行してみましょう。
    Image(systemName: "die.face.3")
    
    変更する場所

    実行してみると出目が3に変わっているはずです。


  2. 変数を追加
    "die.face.1"の数値を変更すれば出目が変わるということがわかったので、そこを変数に書き換えましょう。まずはその変数を宣言します。
    @State var randomNumber = 1
    
    追記する場所


  3. 数値を変数にする
    では、先ほど宣言した変数を画像の数値に対応させます。
    Imageを以下のように変更してください。バックスラッシュはoption + ¥で打てます。
    Image(systemName: "die.face.\(randomNumber)")
    
    追記する場所


  4. ボタンを押したら出目を変更する
    次にボタンを押したら出目を適当に4に変えてみましょう。
    ボタンを押したら呼ばれるところに、以下のようにrandomNumber4に変更してみます。
    Button {
        print("ボタンが押されたよ")
    +   randomNumber = 4
    } label: {
    
    追記する場所

    実行してボタンを押してみると、出目が1から4になるはずです。


  5. ランダムにしてみる
    今は、ボタンを押すと強制的に4になりますが、これをランダムに変更してみましょう。
    先ほど追加したrandomNumber = 4を以下のように変更してみてください。
    randomNumber = Int.random(in: 1...6)
    
    追記する場所

    実行してみると、ボタンを押すたびに出目が変わるかと思います。

アニメーション実装

現時点では、ボタンを押したら一瞬で出目が変わるので、前回と同じで目立った場合やボタンを押せたのかどうか分かりにくいです。そのため、ボタンを押したらサイコロを振ってる感じをアニメーションで作ってあげましょう。
では、どのようなアニメーションにするかというと、ボタンを押してから0.5秒間は、適当な出目を高速で切り替えて、0.5秒経ったら止まるというアニメーションを加えていきたいと思います。

どのように実装するのかというと、ボタンを押したら出目を0.1秒ごとにランダムに変更します。そして、0.5秒経ったら止めます。これでいい感じのアニメーションっぽくなります。
では実装していきましょう。

  1. 変数を追加
    0.1秒ごとに処理をするにはTimerを使います。
    以下のようにtimerという変数を宣言してください。
    @State var timer: Timer?
    
    追記する場所


  2. 0.1秒ごとに出目を変更
    randomNumberにランダムな値を入れている箇所を以下のように変更してください。
    Button {
        print("ボタンが押されたよ")
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
    	randomNumber = Int.random(in: 1...6)
        }
    } label: {
    
    追記する場所

    これで実行してみると、出目がずっと切り替わるようになっているかと思います。

  3. 0.5秒後に止める
    これでは出目がずっと止まらないので、0.5秒経ったらタイマーを止めましょう。
    ボタンを押されたときに呼ばれるブロックに以下のようにコードを追記してください。
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        timer?.invalidate()
        timer = nil
    }
    
    追記する場所



    これでいい感じにアニメーションが動くようになりました!
    実行して確認してみましょう。

バグ修正

現状、このアプリにはバグがあります。
それは、2回連続タップすると、一生止まらなくなるバグです。

なぜか。簡単に説明すると、2回連続タップした時、1回目に押したタイマーが止まる前に、2回目のタイマーが起動するため、1回目のタイマーを止めるタイミングを失ってしまうからです。

では、どのように対応するのかというと、そもそも2回連続タップさせないようにしましょう。

  1. ボタンを無効にする
    2回連続タップさせないようにするには、1回目押されてから、0.5秒間はボタンを無効にするという処理を入れなければなりません。
    ボタンを無効にするには、以下のように.disabled(true)というモディファイアをButtonにつけます。
    Button {
        //省略
    } label: {
        Text("サイコロを振る")
    	//省略
    }
    +.disabled(true)
    
    追記する場所

    実行してボタンを押してみると、全く反応しなくなってると思います。

  2. 変数を用意
    ボタンを押して0.5秒間はtrueで、それ以外はfalseという変数を用意したいので、Bool型の
    以下のように変数を宣言してください。
    @State var isRolling = false
    
    追記する場所


  3. Boolを切り替える
    ボタンを押してから0.5秒間trueにして、0.5秒後にfalseにしたいので、以下のようにコードを追記してください。
    Button {
        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
        }
    } label: {
    
    追記する場所


  4. isRollingを.disabledに反映
    isRollingを、.disabledに反映させましょう。
    .disabled(isRolling)
    
    追記する場所


  5. 実行して確認
    最後に実行して確認しましょう。
    2回タップしても止まらなくなることは無くなっているかと思います。

これでサイコロアプリは完成です!

リファクタリング

最後こリファクタリングしましょう。
リファクタリングというのは、アプリの動作や見た目を変えずに、コードを見やすく整理したり、保守性を高めたりすることです。

処理を関数化する

  1. 関数を作成
    まずは、処理とViewの処理を分けましょう。
    var body: some View {のブロックの下に、以下のコードを追記してください。
    func playDice() {
    }
    
    追記する場所


  2. 処理を移動
    Button {の中に書いてある処理を全て先ほど作成した関数の中に移動させましょう。
    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
        }
    }
    
    追記する場所


  3. 関数を呼ぶ
    Button {の中で先ほど作成した関数を呼びましょう。
    Button {
        playDice()
    } label: {
    
    追記する場所


  4. 動作確認
    リファクタリングしたら必ず動作確認しましょう。
    おそらく問題なく今まで通り動いているかと思います。

privateをつける

そのstructの中でしか使わない変数や定数、関数はprivateをつけましょう。

  1. 変数にprivateをつける
    @State private var randomNumber = 1
    @State private var timer: Timer?
    @State private var isRolling = false
    
    変更する場所


  2. 関数にprivateをつける
    関数にもprivateをつけられます。以下のようにprivateをつけましょう。
    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 {
                playDice()
            } label: {
                Text("サイコロを振る")
                    .padding()
                    .background(.orange)
                    .foregroundColor(.black)
                    .cornerRadius(10)
            }
            .disabled(isRolling)
            Spacer()
        }
        .padding()
    }
    
    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
        }
    }
}