SwiftUI vs UIKit
Appleのモダンな宣言的フレームワークSwiftUIと、実戦で鍛えられたUIKitを徹底比較。2025年、どちらのフレームワークを選ぶべきか?
リアクティブプログラミングのパラダイム — ストリーム、オペレータ、サブスクライバー
Swift 5.5ネイティブの並行処理 — 読みやすく、安全で、モダン
| カテゴリー | Combine | Async/Await |
|---|---|---|
| パフォーマンス | 8/10 | 9/10 |
| 学習のしやすさ | 4/10 | 9/10 |
| エコシステム | 8/10 | 9/10 |
| コミュニティ | 7/10 | 9/10 |
| 求人市場 | 7/10 | 9/10 |
| 将来性 | 6/10 | 10/10 |
// Combine - ライブ検索によるフォームバリデーション
import Combine
import Foundation
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [String] = []
@Published var isLoading = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
private let searchService: SearchService
init(searchService: SearchService) {
self.searchService = searchService
setupSearch()
}
private func setupSearch() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.handleEvents(receiveOutput: { [weak self] _ in
self?.isLoading = true
self?.errorMessage = nil
})
.flatMap { [weak self] query -> AnyPublisher<[String], Never> in
guard let self else { return Just([]).eraseToAnyPublisher() }
return self.searchService.search(query: query)
.catch { [weak self] error -> Just<[String]> in
self?.errorMessage = error.localizedDescription
return Just([])
}
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.sink { [weak self] results in
self?.isLoading = false
self?.results = results
}
.store(in: &cancellables)
}
}// Async/Await - 並列APIコールとエラー処理
import Foundation
// Actorによるスレッドセーフなキャッシュ
actor NetworkCache {
private var cache: [URL: Data] = [:]
func get(_ url: URL) -> Data? { cache[url] }
func set(_ url: URL, data: Data) { cache[url] = data }
}
struct UserDashboard {
let user: User
let posts: [Post]
let notifications: [Notification]
}
@MainActor
class DashboardViewModel: ObservableObject {
@Published var dashboard: UserDashboard?
@Published var isLoading = false
@Published var error: Error?
private let cache = NetworkCache()
func loadDashboard(userId: String) async {
isLoading = true
error = nil
do {
// async letによる並列読み込み
async let user = fetchUser(id: userId)
async let posts = fetchPosts(userId: userId)
async let notifications = fetchNotifications(userId: userId)
// すべて同時に開始し、すべての完了を待つ
dashboard = UserDashboard(
user: try await user,
posts: try await posts,
notifications: try await notifications
)
} catch {
self.error = error
}
isLoading = false
}
private func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
private func fetchPosts(userId: String) async throws -> [Post] {
let url = URL(string: "https://api.example.com/users/\(userId)/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
private func fetchNotifications(userId: String) async throws -> [Notification] {
let url = URL(string: "https://api.example.com/users/\(userId)/notifications")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Notification].self, from: data)
}
}2025年以降の新規プロジェクトではAsync/Awaitを選びましょう — より読みやすく、エラーが起きにくく、Appleが積極的に開発を進めている方向性です。複雑なリアクティブフロー(ライブ検索、フォームバリデーションのパイプラインなど)にはCombineを引き続き使いましょう。両者は組み合わせて使うと非常に相性が良いです。
無料相談を受ける段階的な移行が推奨されます。新機能はasync/awaitで書けます。既存のCombineコードはそのまま動作し続けます。AsyncPublisherを使えばCombineのストリームをasyncシーケンスに変換することも可能です。