🪶

Combine FWを使う場面

2024/07/16に公開

subscribeするもの

subscribeとは、購読するという意味。Streamでデータを扱うビジネスロジックで使う場面がある。通信するものもあれば、タイマーだったり。常にデータがリアルタイムの状態のもの。ずっと読み込んでいるので、購読するという意味なのでしょう。

https://developer.apple.com/documentation/combine/publisher

https://www.youtube.com/watch?v=tbzR-eHr6oo&list=PLWHegwAgjOkoIMgZ7QF_SHUtEB_rWXtH0

動画を見てみると、Streamを使ってデータを出力すると紹介されている。Streamを開始すると、サブスクリプションを購読する。途中で停止することもできる。今回だとこれをメモリに保存して、動画で紹介しているような、購読した値はパブリッシャーから受け取り、配列に保存する。

以下の例だと、Combineフレームワークを使用して、Streamでデータを時間を開始して、読み込み、状態を管理するクラスを作成しました。ObservableObjectプロトコルに準拠する必要があるので、class StopwatchManager の後につける。

import Foundation
import Combine

class StopwatchManager: ObservableObject {
    @Published var elapsedTime: TimeInterval = 0
    @Published var savedTimes: [TimeInterval] = []
    private var timer: AnyCancellable?
    private var startTime: Date?
    // start timer
    func start() {
        startTime = Date()
        timer = Timer.publish(every: 0.1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self, let startTime = self.startTime else { return }
                self.elapsedTime = Date().timeIntervalSince(startTime)
            }
    }
    // stop timer
    func pause() {
        timer?.cancel()
        startTime = nil
    }
    // reset timer
    func reset() {
        timer?.cancel()
        startTime = nil
        elapsedTime = 0
    }
    // array.append(value)
    func saveTime() {
        savedTimes.append(elapsedTime)
    }
}

UIに表示するときは、@StateObjectを使って呼び出す。これで、タイマーの購読ができる。

import SwiftUI

struct ContentView: View {
    @StateObject private var stopwatch = StopwatchManager()
    
    var body: some View {
        VStack {
            Text(timeString(stopwatch.elapsedTime))
                .font(.system(size: 50, weight: .bold, design: .monospaced))
                .padding()
            
            HStack(spacing: 20) {
                Button(action: stopwatch.start) {
                    Image(systemName: "play.circle.fill")
                        .resizable()
                        .frame(width: 50, height: 50)
                }
                
                Button(action: stopwatch.pause) {
                    Image(systemName: "pause.circle.fill")
                        .resizable()
                        .frame(width: 50, height: 50)
                }
                
                Button(action: stopwatch.reset) {
                    Image(systemName: "stop.circle.fill")
                        .resizable()
                        .frame(width: 50, height: 50)
                }
                
                Button(action: stopwatch.saveTime) {
                    Image(systemName: "square.and.arrow.down.fill")
                        .resizable()
                        .frame(width: 50, height: 50)
                }
            }
            .padding()
            
            List {
                ForEach(stopwatch.savedTimes, id: \.self) { time in
                    Text(timeString(time))
                }
            }
        }
    }
    
    private func timeString(_ time: TimeInterval) -> String {
        let hours = Int(time) / 3600
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        let centiseconds = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
        return String(format: "%02d:%02d:%02d.%02d", hours, minutes, seconds, centiseconds)
    }
}

#Preview {
    ContentView()
}

動作はこんな感じですね。

まとめ

Combineフレームワークの使い所は、リアルタイムのデータをサブスクライブするときですね。非同期にデータを読み込んで、1度しかデータを取得する必要がなければ、async/awaitの方が良い。

Discussion