Appleが2019年に発表したSwiftUIとCombineは、iOSアプリ開発のパラダイムを根本的に変えました。従来のUIKitによる命令的UIプログラミングから、SwiftUIの宣言的UIプログラミングへ。そしてコールバックやデリゲートパターンが中心だった非同期処理から、Combineによるリアクティブストリーム処理へ。この2つのフレームワークを組み合わせることで、より直感的で保守性の高いコードを書けるようになります。
本記事では、SwiftUIとCombineの基礎から実践的な活用方法まで、現場で使える知識を体系的に解説します。
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(データの変換処理)の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チェーンで変換処理を定義しています。
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アーキテクチャと非常に相性が良いです。以下はタスク管理アプリの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)でサブスクリプションのライフサイクルを管理します.receive(on: DispatchQueue.main)でメインスレッドに切り替えてからUIに反映させますSwiftUIとCombineの組み合わせは、iOS開発において強力なリアクティブプログラミング基盤を提供します。宣言的UIとリアクティブなデータフローにより、コードの可読性と保守性が大幅に向上します。ただし、学習コストは決して低くないため、まずは小さなプロジェクトで経験を積み、段階的に既存プロジェクトに導入していくことをおすすめします。