一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • SwiftUIとCombineで実現するリアクティブプログラミング入門

SwiftUIとCombineがiOS開発を変えた

Appleが2019年に発表したSwiftUIとCombineは、iOSアプリ開発のパラダイムを根本的に変えました。従来のUIKitによる命令的UIプログラミングから、SwiftUIの宣言的UIプログラミングへ。そしてコールバックやデリゲートパターンが中心だった非同期処理から、Combineによるリアクティブストリーム処理へ。この2つのフレームワークを組み合わせることで、より直感的で保守性の高いコードを書けるようになります。

本記事では、SwiftUIとCombineの基礎から実践的な活用方法まで、現場で使える知識を体系的に解説します。

SwiftUIの基本概念

宣言的UIの考え方

SwiftUIでは「UIの状態」を定義すれば、フレームワークが自動的にUIを更新してくれます。UIKitのように手動でビューの更新処理を書く必要がありません。

struct ContentView: View {
@State private var count = 0

var body: some View {
VStack(spacing: 20) {
Text("カウント: \(count)")
.font(.largeTitle)

Button("増加") {
count += 1
}
.buttonStyle(.borderedProminent)
}
}
}

@Stateプロパティラッパーで宣言されたcountが変更されると、SwiftUIが自動的にビューを再描画します。この「状態が変わればUIが自動で追従する」というのが宣言的UIの核心です。

データフローの制御

SwiftUIでは、データの所有権と流れを明確にするために複数のプロパティラッパーが用意されています。

// 自ビューが所有する状態
@State private var isLoading = false

// 親ビューから渡される双方向バインディング
@Binding var selectedTab: Int

// ObservableObjectを監視
@StateObject private var viewModel = TaskViewModel()

// 親から渡されたObservableObjectを参照
@ObservedObject var settings: AppSettings

// 環境経由で注入されたオブジェクト
@EnvironmentObject var authManager: AuthManager

特に重要なのは@StateObject@ObservedObjectの使い分けです。ビューが生成(所有)するオブジェクトには@StateObject、外部から受け取るオブジェクトには@ObservedObjectを使います。この区別を誤ると、ビューの再描画時にオブジェクトが再生成されるバグが発生します。

Combineフレームワークの基礎

Publisher、Subscriber、Operatorの三要素

CombineはPublisher(データの発行者)、Subscriber(データの購読者)、Operator(データの変換処理)の3つの要素で構成されるリアクティブフレームワークです。

import Combine

class TaskViewModel: ObservableObject {
@Published var tasks: [Task] = []
@Published var searchText: String = ""
@Published var isLoading: Bool = false

private var cancellables = Set<AnyCancellable>()

init() {
setupSearchPipeline()
}

private func setupSearchPipeline() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.sink { [weak self] query in
self?.searchTasks(query: query)
}
.store(in: &cancellables)
}

private func searchTasks(query: String) {
isLoading = true
// API呼び出しの実装
}
}

@Publishedプロパティラッパーは、値の変更を自動的にパブリッシュするPublisherを生成します。$searchTextでPublisherにアクセスし、Operatorチェーンで変換処理を定義しています。

主要なOperator

Combineで頻繁に使用するOperatorを理解しておくと、効率的なストリーム処理が書けます。

// map: 値を変換
$inputText
.map { $0.trimmingCharacters(in: .whitespaces) }

// filter: 条件に合う値のみ通過
$score
.filter { $0 >= 0 }

// combineLatest: 複数のPublisherの最新値を結合
Publishers.CombineLatest($username, $password)
.map { username, password in
!username.isEmpty && password.count >= 8
}
.assign(to: &$isFormValid)

// flatMap: Publisherを別のPublisherに変換
$userId
.flatMap { id in
APIClient.fetchUser(id: id)
}

SwiftUIとCombineの連携パターン

MVVMアーキテクチャの実装

SwiftUIとCombineの組み合わせは、MVVMアーキテクチャと非常に相性が良いです。以下はタスク管理アプリのViewModel実装例です。

class TaskListViewModel: ObservableObject {
@Published var tasks: [TaskItem] = []
@Published var errorMessage: String?

private let taskService: TaskServiceProtocol
private var cancellables = Set<AnyCancellable>()

init(taskService: TaskServiceProtocol) {
self.taskService = taskService
}

func loadTasks() {
taskService.fetchTasks()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] tasks in
self?.tasks = tasks
}
)
.store(in: &cancellables)
}
}

ビュー側では、このViewModelを@StateObjectで保持し、状態の変化を自動的にUIに反映させます。

struct TaskListView: View {
@StateObject private var viewModel: TaskListViewModel

init(taskService: TaskServiceProtocol) {
_viewModel = StateObject(
wrappedValue: TaskListViewModel(taskService: taskService)
)
}

var body: some View {
List(viewModel.tasks) { task in
TaskRowView(task: task)
}
.overlay {
if viewModel.tasks.isEmpty {
Text("タスクがありません")
.foregroundStyle(.secondary)
}
}
.alert("エラー", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") { viewModel.errorMessage = nil }
} message: {
Text(viewModel.errorMessage ?? "")
}
.onAppear {
viewModel.loadTasks()
}
}
}

実践上の注意点とベストプラクティス

  • メモリリークに注意sinkのクロージャ内では必ず[weak self]を使い、循環参照を防ぎます。store(in: &cancellables)でサブスクリプションのライフサイクルを管理します
  • メインスレッドでのUI更新:API通信の結果などバックグラウンドスレッドで取得したデータは、.receive(on: DispatchQueue.main)でメインスレッドに切り替えてからUIに反映させます
  • async/awaitとの使い分け:Swift 5.5以降では、単純な非同期処理はasync/awaitの方がシンプルに書けます。Combineは複数のイベントストリームの結合や変換など、リアクティブパターンが必要な場面で活用しましょう
  • テスタビリティ:ViewModelにプロトコル経由でサービスを注入することで、ユニットテストが容易になります

まとめ

SwiftUIとCombineの組み合わせは、iOS開発において強力なリアクティブプログラミング基盤を提供します。宣言的UIとリアクティブなデータフローにより、コードの可読性と保守性が大幅に向上します。ただし、学習コストは決して低くないため、まずは小さなプロジェクトで経験を積み、段階的に既存プロジェクトに導入していくことをおすすめします。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア