Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【SwiftUI】カウントアップのアニメーション

 

はじめに

【Combine】Timerの処理をCombineを使って置き換える - Swift・iOSの続きです。Stack Overflowの記事(ios - SwiftUI - Animating count text from 0 to x - Stack Overflow)を参考に、CombineのTimerを使ってカウントアップのアニメーションを実装してみました。

 

本題

サンプルイメージ

f:id:hfoasi8fje3:20210823202529g:plain

 

 

開発環境

 

全体の実装

ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject private var viewModel: ContentViewModel
    
    init(viewModel: ContentViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        VStack {
            Text("\(viewModel.count)")
                .font(.title)
                .fontWeight(.bold)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(viewModel: ContentViewModel())
    }
}

 

ContentViewModel.swift
import Foundation
import Combine

final class ContentViewModel: ObservableObject {
    @Published var count = 0
    private let endCount: Int = 1000
    
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = Timer.publish(every: 0.02, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }

                if self.count < self.endCount {
                    self.count += 1
                } else {
                    self.cancellable?.cancel()
                }
            }
    }
}

 

おまけ:幅広い数字に対応する

サンプルでは1,000になるまで1を足し続けていますが、例えばSuicaアプリのチャージ機能のように、500円〜10,000円の範囲でアニメーションする可能性がある場合、大きい数字になればなるほどアニメーションが完了するまでの時間がかかってしまい、ユーザーを待たせてしまいます。そのため、アニメーションしたい数字に合わせて足す数字を変更して対応してみます。以下のようにViewModelの処理を変更、追加しました。

ContentViewModel.swift

import Foundation
import Combine

final class ContentViewModel: ObservableObject {
    @Published var count = 0
    
    // ここの数字を変更して動作を確認してみてください
    private let endCount: Int = 10000
    
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = Timer.publish(every: 0.02, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                
                // 追加
                let additionNumber = Int(ceil(Double(self.endCount) / 50.0))
                
                if self.count < self.endCount {
                    // 変更
                    self.count += additionNumber
                } else {
                    // 追加
                    self.count = self.endCount
                    self.cancellable?.cancel()
                }
            }
    }
}

 

※追記:アニメーション時の数字の横振れに対応する

タイマーのペースを上げると、数字が横振れしながらカウントアップするように見えてしまいます。等幅フォントに変更すると、横振れを抑えることができます。以下の記事に詳しく記載されており、参考にさせていただきました。

※参考:【SwiftUI】SFフォントを等幅フォントとして扱う方法 - おもちblog

今回のサンプルでは、ContentView.swiftのTextのフォント周りの実装を以下のように変更します。

変更前

Text("\(viewModel.count)")
    .font(.title)
    .fontWeight(.bold)

変更後

Text("\(viewModel.count)")
    .font(Font(UIFont.monospacedDigitSystemFont(ofSize: 30, weight: .bold)))

 

おわりに

「幅広い数字に対応する」で書いたことが今回記事に残したかったことなのですが、読み手からすればしょうもない内容なのでおまけにしました笑

 

参考