概要
SwiftUIにおける<strong>Combine</strong>は、変数の監視、変更の通知、そしてその変更に対する非同期的な処理の流れ(パイプライン)を宣言的に設定できるフレームワークです。
Combineの役割
Combineの主要な機能と、それが「変数の監視、処理を自由に設定できる」という認識にどう結びつくかをご説明します。
通知が発行されてからビューが受け取るまでの間に、Operators(演算子)を使ってデータを変換したり、フィルタリングしたり、結合したりといった「処理を自由に設定」できます。
変数の監視と公開(Publishers):ObservableObjectと@PublishedプロパティがCombineの核となります。@Publishedを付けた変数はPublisher(発行者)となり、その値が変更されるたびに通知(イベント)を発行します。これにより、変数の「監視」が実現します。
通知の受信と処理(Subscribers & Operators):@ObservedObjectや@EnvironmentObject、@StateObjectが付いたView(ビュー)は、これらの通知を自動的に受け取って(Subscribeして)再描画します。
Combineが実現するもの
| 機能 | 概要 |
| データフローの宣言 | データの流れ(いつ、どこで、どのようにデータが変化し、どこで使われるか)をコードで明確に定義します。 |
| 非同期処理 | ネットワークリクエストやタイマーなどの非同期イベントの処理を、構造化された統一的な方法で扱えます。 |
| リアクティブプログラミング | データの変化に「反応」して処理が実行される、リアクティブ(反応的)なアーキテクチャを可能にします。 |
🔧 構成概要
|
1 2 3 4 |
TokenManager(ObservableObject) ├ @Published var token: String? ├ @Published var expiresAt: Date? └ CombineでexpiresAtを監視 → 残り5分未満で自動更新 |
🧩 コード例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import Foundation import Combine @MainActor class TokenManager: ObservableObject { static let shared = TokenManager() @Published private(set) var token: String? @Published private(set) var expiresAt: Date? private var cancellables = Set<AnyCancellable>() private init() { // 起動時にトークン監視をセットアップ setupTokenWatcher() } func setupTokenWatcher() { // expiresAt が変化したら監視 $expiresAt .compactMap { $0 } // nil を除外 .flatMap { [weak self] expDate -> AnyPublisher<Void, Never> in guard let self = self else { return Empty().eraseToAnyPublisher() } // 残り時間を計算 let interval = expDate.timeIntervalSinceNow - 5 * 60 // 5分前 if interval <= 0 { // すでに期限切れなら即時更新 return Just(()).eraseToAnyPublisher() } else { // interval 秒後にイベントを発火 return Just(()) .delay(for: .seconds(interval), scheduler: RunLoop.main) .eraseToAnyPublisher() } } .sink { [weak self] in Task { await self?.refreshTokenIfNeeded() } } .store(in: &cancellables) } func refreshTokenIfNeeded() async { guard let expiresAt = expiresAt else { await fetchNewToken() return } let remaining = expiresAt.timeIntervalSinceNow if remaining < 5 * 60 { await fetchNewToken() } } func fetchNewToken() async { // 実際はサーバーから取得 print("🔁 トークンを更新します") self.token = "new_token_\(UUID().uuidString)" self.expiresAt = Date().addingTimeInterval(30 * 60) // 有効期限30分 } } |
⚙️ 仕組みの説明
expiresAtが更新されるたびに Combine が購読開始。- 残り時間から「5分前にJustイベントを発火」する Publisher を生成。
- そのタイミングで
refreshTokenIfNeeded()を呼び出す。 - 更新すると
expiresAtが再度変わるので、監視がリセットされる。
これで「トークンの期限が近づくと自動更新」のループが自然に構築されます!
🧠 メリット
- ViewやAPI処理から独立して動く(完全自動)
- 1か所で有効期限を管理できる
- Combineなのでメモリリークしにくい
- SwiftUIの
.task {}やTimerを使うより安定
🧪 Viewでの使い方例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct TokenDebugView: View { @StateObject var tm = TokenManager.shared var body: some View { VStack(spacing: 16) { Text("Token: \(tm.token ?? "none")") Text("Expires: \(tm.expiresAt?.description ?? "-")") Button("Fetch New Token") { Task { await tm.fetchNewToken() } } } .padding() .onAppear { Task { await tm.fetchNewToken() } } } } |
起動してから30分ごとに自動更新、
または「Fetch New Token」ボタンで手動更新できます。
🧩 拡張のヒント
このCombine監視を応用すると:
- トークン更新時に他のAPIクラスへ通知(
NotificationCenterや@Published購読) - 期限切れ検出でUIにアラート表示
- 複数のアプリ共通トークンでも安全管理
なども可能です。
修飾子
| 修飾子 | 役割 |
.compactMap { $0 } | 「nilを無視して値だけ通す」 |
.flatMap { ... } | 「受け取った値を新しいPublisherに変換して流す」 渡す値の型が違う時にキャストする所 |
.eraseToAnyPublisher() | 「型をシンプルに隠す」 |
.sink { [weak self] _ in ... } | 「最終的な購読者(subscriber)」 実行処理を書く所 |
.store(in: &cancellables) | 「購読を保持(解除されないようにする)」 Combine継続処理させる条件 |
✅ 拡張例
もし将来的に「アプリがバックグラウンドになったら停止」「フォアグラウンドに戻ったら再開」もしたければ、
以下のように Combine で NotificationCenter を監視して制御できます。
|
1 2 3 4 5 6 7 |
NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification) .sink { [weak self] _ in self?.setupAutoRefreshPipeline() } .store(in: &cancellables) NotificationCenter.default.publisher(for: UIScene.didEnterBackgroundNotification) .sink { [weak self] _ in self?.stopAutoRefresh() } .store(in: &cancellables) |
✅ 自動で1分おきにAPIを再取得するViewModel例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
import Foundation import Combine @MainActor final class AutoRefreshWeatherViewModel: ObservableObject { @Published var weather: OpWeather? = nil @Published var errorMessage: String? = nil @Published var isLoading: Bool = false private let apiClient = GetOpenWeatherMapAPI() // あなたの既存のAPI取得クラス private var cancellables = Set<AnyCancellable>() private var currentTask: Task<Void, Never>? = nil // 自動更新間隔(ここでは60秒) private let refreshInterval: TimeInterval = 60 init() { setupAutoRefreshPipeline() } /// Combineで1分おきに更新するパイプラインを構築 private func setupAutoRefreshPipeline() { // 1分おきにVoidを流すPublisherを作る Timer.publish(every: refreshInterval, on: .main, in: .common) .autoconnect() .prepend(Date()) // 起動時にも即1回発火 .sink { [weak self] _ in guard let self else { return } self.triggerRefresh() } .store(in: &cancellables) } /// 実際にAPIを呼ぶ関数 private func triggerRefresh() { // 前のタスクがまだ走っていればキャンセル currentTask?.cancel() currentTask = Task { await fetchWeather() } } /// API取得処理 private func fetchWeather() async { if Task.isCancelled { return } isLoading = true errorMessage = nil defer { isLoading = false } do { // ここがあなたの既存API呼び出し関数 let result = try await apiClient.fetchWeather(for: "Tokyo") // 結果をUIへ反映 self.weather = result } catch { if Task.isCancelled { return } self.errorMessage = "取得失敗: \(error.localizedDescription)" } } /// 明示的に停止したい場合(ViewのonDisappearなどで呼ぶ) func stopAutoRefresh() { currentTask?.cancel() cancellables.removeAll() print("🧹 自動更新を停止しました") } deinit { stopAutoRefresh() } } |
🧭 解説ポイント
| 項目 | 内容 |
|---|---|
| Timer.publish | Combineの TimerPublisher を使って定期的にイベントを流す。autoconnect() で自動起動。 |
| .prepend(Date()) | 起動時に即1回発火させる(初期表示用)。 |
| Taskキャンセル | 新しい更新が始まる前に前の通信を止める。これによりAPIの多重呼び出し防止。 |

コメント