LoginSignup
4

More than 1 year has passed since last update.

posted at

updated at

SwiftUIを使って学習がてらタイマーアプリを作ってみよう2

SwiftUIを使って学習がてらタイマーアプリを作ってみよう Part2

カウントアップ(Timer)処理を実装してみよう

前置き 
前回投稿したPart1の続きです。
今回のゴールとしては
TextとButtonを置きButtonをタップしたらカウンドダウンが始まりTextの表示が更新される所までとしようと思います。
次回はカウントダウンとプログレスバーの方を実装です。

開発環境

Mac (OS Big Sur version:11.3.1)
Xcode (version:12.5)
Swift (version: 5.4)

Timer用画面を作成(Test用)

とりあえずは簡単にタイマーのカウントが表示されるTextとタイマー処理を開始するボタンがあるだけの画面を作成

TimerModel.swift
struct TimerView: View {
    var body: some View {
        // 縦方向に並べる
        VStack(alignment: .center){
            // Show Timer Count
            Text("Timer Count")
                .padding(10)
            // Start Timer Button
            Button(action: {
               //TODO タイマー開始を記述
            }){
                Text("Start Timer")
            }

        }
    }
}

Timerを試してみる

Timer管理用のクラスを作成し、startCountUpでタイマー処理の開始。
stop()でタイマー処理を停止させるようにしている

TimerModel.swift
import Foundation
import Combine

class TimerModel: ObservableObject{
    @Published var timer: Timer!
    @Published var count: Int = 0


    func startCountUp(){
        // Timerの実態があるときは停止させる
        self.timer?.invalidate()
        // count初期化
        self.count = 0
        // Timer取得
        self.timer = Timer.scheduledTimer(withTimeInterval:0.01, repeats: true){ _ in
            self.count += 1
        }
    }

    func stop(){
        timer?.invalidate()
        timer = nil
    }
}

@Published var count

Publishedタグをつけた変数の値が変わるとView内の表示も自動で切り替わるようになる

Timer.scheduledTimer(withTimeInterval:0.01, repeats: true){ _ in
self.count += 1
}

↑が実際のタイマー(繰り返し)処理となる。
scheduledTimer(withTimeInterval: interval, repeats: true, block:{timer in })関数で最後をラムダ式で描いたもので各変数の
ざっくりとした説明は

タイマー処理
scheduledTimer(
        interval:実行までの時間(1.0秒単位)
    repeats: 繰り返しをするか
    bolock{timer in }:実行された時に呼ばれる(変数としてTimerが取得できるが、今回は利用しないため _ で変数名を省略している)
)

となる

Timer + Combineを試してみる

Timerについて調べているとどうやらXcode11以降+Combineに対応したOSバージョンと言う条件こそあるものの
Combineの仕組みに対応するように拡張されているみたいなのでそちらを使うようにしてみる
*Combineとはざっくり言うとRxSwiftに似たIOS13以上の端末に対応している非同期フレームワークの事らしい。詳細はこちらのサイトに記載してくれてる

 任意のタイミングでTimerの開始・停止を管理したいと思っていたのだが、
調べが甘いせいか View(SwiftUI).onReceive(Publisher, perform: { _ in code })にTimer.TimerPublisherを入れる
やり方だと常時動いていて条件よって処理の実行・不実行をするぐらいしか見つからなかったので、
今回はTimer側でreceiveとsinkを設定を行う形で実装をしました。

「Timerを試してみる」で作ったソースを以下のように改造した

TimerModel.swift
import Foundation
import Combine

class TimerModel: ObservableObject{
    @Published var count: Int = 0
    @Published var timer: AnyCancellable!

    // タイマーの開始
    func start(_ interval: Double = 1.0){
        print("start Timer")

        // TimerPublisherが存在しているときは念の為処理をキャンセル
        if let _timer = timer{
            _timer.cancel()
        }

        // every -> 繰り返し頻度
        // on -> タイマー処理が走るLoopを指定
        // in: -> 入力処理モードの設定 .defalut:操作系の入力と同様に処理をするらしい : .common それ以外
        // .defalutを指定していると処理が遅くなることがある
        timer = Timer.publish(every: interval, on: .main, in: .common)
            // 繰り返し処理の実行
            .autoconnect()
            //  レシーバーが動くスレッドを指定しているのだと思われる
            //  .main -> メインスレッド(UI) , .global() -> 別スレッド
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: ({_ in
                // 設定した時間ごとに呼ばれる
                self.count += 1
        }))

    }

    // タイマーの停止
    func stop(){
        print("stop Timer")
        timer?.cancel()
        timer = nil
    }

}

TimerModel.swiftでやっていることは単純で
start(interval)を呼ぶとタイマー処理を実行し、
stop()を呼ぶことでタイマーを停止する。

後は最初に作った画面のTextにTimerModel.countを表示するように
Buttonのactionでstart()/stop()を呼ぶようにしてあげればButtonを押すとTimerModel.countがカウントアップされるようになる。
また、PublishedタグをつけているのでTimerModel.countが更新されるとView側も自動で更新されるようになる。

TimerView.swift
import SwiftUI

struct TimerView: View {
    @EnvironmentObject var timerContorller: TimerModel
    // タイマー開始時間

    var body: some View {
        // 縦方向に並べる
        VStack(alignment: .center){
            // Show Timer Count
            Text("\(timerContorller.count)")
                .padding(10)
            // Start/Stop Timer Button
            Button(action: {
                if(timerContorller.timer == nil){
                    timerContorller.start(0.1)
                }else{
                    timerContorller.stop()
                }
            }){
                // timerの状態でラベルの文字を切り替える
                Text("\((timerContorller.timer != nil) ? "Stop Timer" : "Start Timer")")
            }
        }
    }
}

参考・引用元

[Swift] はじめてのCombine | Apple製の非同期フレームワークを使ってみよう
SwiftUIとCombineフレームワークその1
【Swift】Timerがズレる (遅延する) 時の原因と対処法
【iOS】Combineフレームワークまとめ

シリーズ:「SwiftUIの学習でタイマーアプリを作ってみよう」のリンク

part1:プロジェクトの作成とGitHubに登録
[part2: Timerを利用したカウント処理の実装] ← 今ここ
part3:計測画面でのプログレスバーの実装
part4:スピナー実装
part5:ライブラリから音楽を選択して再生
part6:タイマー処理のバックグランド対応
part7:音楽のバックグランド再生対応
part8:PageViewを作成
part9:PageViewのインジゲータを作成

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4