# Combine vs async/await ใน Swift: รูปแบบการย้ายระบบแบบค่อยเป็นค่อยไป > คู่มือฉบับสมบูรณ์สำหรับการย้ายจาก Combine ไปยัง async/await ใน Swift: กลยุทธ์แบบค่อยเป็นค่อยไป รูปแบบการเชื่อมโยง และการอยู่ร่วมกันของกระบวนทัศน์ในโค้ดเบส iOS - Published: 2026-03-13 - Updated: 2026-05-01 - Author: SharpSkill - Tags: swift, ios, combine, async-await, migration - Reading time: 14 min --- การมาถึงของ Swift Concurrency พร้อมกับ async/await ได้เปลี่ยนแปลงแนวปฏิบัติการเขียนโปรแกรมแบบไม่พร้อมกันบน iOS สำหรับโครงการที่ใช้ Combine คำถามเรื่องการย้ายระบบเกิดขึ้นโดยธรรมชาติ จำเป็นต้องเขียนใหม่ทั้งหมดหรือไม่ สองแนวทางสามารถอยู่ร่วมกันได้หรือไม่ รูปแบบใดที่ช่วยให้การเปลี่ยนผ่านราบรื่น คู่มือนี้สำรวจกลยุทธ์การย้ายระบบแบบค่อยเป็นค่อยไป ที่ช่วยให้นำ async/await มาใช้ได้โดยไม่ละทิ้ง Combine อย่างกะทันหัน > **สิ่งที่คู่มือนี้ครอบคลุม** > > คู่มือนี้นำเสนอรูปแบบที่เป็นรูปธรรมสำหรับการย้ายระบบแบบค่อยเป็นค่อยไปจาก Combine ไปยัง async/await พร้อมตัวอย่างการเชื่อมโยงสองทิศทางและกลยุทธ์การอยู่ร่วมกันที่เหมาะสมกับโค้ดเบสที่มีอยู่ ## ทำความเข้าใจความแตกต่างพื้นฐาน ก่อนเริ่มการย้ายระบบ การทำความเข้าใจสิ่งที่แยก Combine ออกจาก async/await เป็นเรื่องสำคัญ สองแนวทางนี้ตอบสนองความต้องการที่แตกต่างกัน และกรณีการใช้งานบางอย่างยังคงเหมาะกับ Combine มากกว่า ### แบบจำลองทางความคิดของ Combine Combine อิงจากแบบจำลองสตรีมข้อมูล Publisher ปล่อยค่าออกมาตามเวลา ตัวดำเนินการแปลงค่าเหล่านั้น และ Subscriber รับผลลัพธ์สุดท้าย แบบจำลองนี้โดดเด่นสำหรับสตรีมต่อเนื่อง เช่น เหตุการณ์ UI การแจ้งเตือน หรือ WebSocket ```swift // CombineExample.swift // Event stream with Combine - stream-based model import Combine class SearchViewModel { @Published var searchText = "" private var cancellables = Set() // Combine excels for continuous streams with transformations func setupSearch() { $searchText // Wait 300ms pause in typing .debounce(for: .milliseconds(300), scheduler: RunLoop.main) // Ignore consecutive duplicates .removeDuplicates() // Filter searches that are too short .filter { $0.count >= 3 } // Transform text into network request .flatMap { query in self.searchAPI(query: query) // Local error handling .catch { _ in Just([]) } } // Final subscription .sink { results in self.updateUI(with: results) } .store(in: &cancellables) } private func searchAPI(query: String) -> AnyPublisher<[SearchResult], Error> { // Network implementation } } ``` โค้ดนี้แสดงให้เห็นพลังของ Combine: การเชื่อมตัวดำเนินการแบบประกาศเข้าด้วยกันเพื่อประมวลผลสตรีมเหตุการณ์ที่ต่อเนื่อง ### แบบจำลองทางความคิดของ async/await Async/await ใช้แบบจำลองตามลำดับ: การดำเนินการเริ่มต้น โค้ดรอผลลัพธ์ จากนั้นดำเนินการต่อ แบบจำลองนี้เป็นธรรมชาติมากกว่าสำหรับการดำเนินการครั้งเดียว เช่น คำขอเครือข่ายที่แยกออก หรือการอ่านไฟล์ ```swift // AsyncAwaitExample.swift // One-off operations with async/await - sequential model import Foundation actor SearchService { // async/await excels for sequential operations func performSearch(query: String) async throws -> [SearchResult] { // Pre-validation - clear sequential reading guard query.count >= 3 else { return [] } // Network request with await let url = URL(string: "https://api.example.com/search?q=\(query)")! let (data, response) = try await URLSession.shared.data(from: url) // Response verification guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw SearchError.invalidResponse } // Result decoding let results = try JSONDecoder().decode([SearchResult].self, from: data) return results } } ``` การอ่านเป็นเชิงเส้น ข้อผิดพลาดแพร่กระจายอย่างเป็นธรรมชาติด้วย `try` และกระแสการดำเนินการเข้าใจได้ทันที > **เมื่อใดควรเลือกแต่ละแนวทาง** > > Combine ยังคงเหมาะสมสำหรับสตรีมต่อเนื่อง (เหตุการณ์ UI, ตัวจับเวลา, WebSocket) Async/await เหมาะกับการดำเนินการครั้งเดียวมากกว่า (คำขอ API, การอ่านไฟล์, การคำนวณที่แยกออก) ## การเชื่อมโยงจาก Combine ไปยัง async/await ขั้นตอนแรกของการย้ายระบบมักประกอบด้วยการบริโภค Publisher ที่มีอยู่ในโค้ด async/await Swift มอบเครื่องมือดั้งเดิมสำหรับการเชื่อมโยงนี้ ### การใช้ AsyncSequence ด้วย Publisher.values ตั้งแต่ Swift 5.5 ทุก Publisher เปิดเผยพร็อพเพอร์ตี้ `.values` ที่คืนค่า `AsyncPublisher` ซีเควนซ์แบบไม่พร้อมกันนี้ช่วยให้สามารถวนซ้ำค่าที่ปล่อยออกมาด้วยลูป `for await` ```swift // BridgingCombineToAsync.swift // Publisher → AsyncSequence conversion via .values import Combine class NotificationObserver { private let notificationPublisher: AnyPublisher init() { // Existing Combine Publisher notificationPublisher = NotificationCenter.default .publisher(for: UIApplication.didBecomeActiveNotification) .eraseToAnyPublisher() } // Consuming the Publisher with async/await func observeNotifications() async { // .values converts the Publisher to AsyncSequence for await notification in notificationPublisher.values { // Process each notification await handleAppBecameActive(notification) } // This line is never reached for an infinite Publisher } private func handleAppBecameActive(_ notification: Notification) async { // Async processing logic } } ``` แนวทางนี้รักษา Publisher เดิมไว้ในขณะที่อนุญาตให้บริโภคในบริบทแบบไม่พร้อมกัน ### การได้ค่าเดียวด้วย firstValue สำหรับ Publisher ที่ปล่อยค่าเดียว (เช่น คำขอเครือข่าย) พร็อพเพอร์ตี้ `.values.first(where:)` หรือ extension แบบกำหนดเองช่วยลดความซับซ้อนของการเชื่อมโยง ```swift // SingleValueBridging.swift // Extension to extract a single value from a Publisher import Combine extension Publisher where Failure == Never { // Awaits and returns the first emitted value var firstValue: Output { get async { await withCheckedContinuation { continuation in var cancellable: AnyCancellable? cancellable = self.first() .sink { value in continuation.resume(returning: value) cancellable?.cancel() } } } } } extension Publisher { // Throwing version for Publishers with errors var firstValueThrowing: Output { get async throws { try await withCheckedThrowingContinuation { continuation in var cancellable: AnyCancellable? cancellable = self.first() .sink( receiveCompletion: { completion in if case .failure(let error) = completion { continuation.resume(throwing: error) } cancellable?.cancel() }, receiveValue: { value in continuation.resume(returning: value) } ) } } } } // Usage in async code class UserRepository { private let apiClient: APIClient func fetchCurrentUser() async throws -> User { // Consume an existing Publisher asynchronously try await apiClient.userPublisher().firstValueThrowing } } ``` Extension นี้รวบรวมความซับซ้อนของการเชื่อมโยงและนำเสนอ API ที่สะอาด ## การเชื่อมโยงจาก async/await ไปยัง Combine การย้ายในทิศทางตรงกันข้ามก็จำเป็นเช่นกัน: การบริโภคโค้ด async ในไปป์ไลน์ Combine ที่มีอยู่ ### การสร้าง Publisher จากฟังก์ชัน async แนวทางที่ตรงที่สุดใช้ `Future` ผสมกับ `Task` เพื่อรวบรวมการเรียกใช้ async ```swift // BridgingAsyncToCombine.swift // async → Publisher conversion via Future import Combine extension Publisher { // async flatMap operator for Combine pipelines func asyncMap( _ transform: @escaping (Output) async throws -> T ) -> AnyPublisher { flatMap { value in Future { promise in Task { do { // Execute the async transformation let result = try await transform(value) promise(.success(result)) } catch { promise(.failure(error)) } } } } .eraseToAnyPublisher() } } // Usage in a Combine pipeline class ImageProcessor { @Published var selectedImageURL: URL? private var cancellables = Set() func setupProcessingPipeline() { $selectedImageURL .compactMap { $0 } // Use an async function in the Combine pipeline .asyncMap { url in // downloadImage is an async function try await self.downloadImage(from: url) } .asyncMap { imageData in // processImage is also async try await self.processImage(imageData) } .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { print("Error: \(error)") } }, receiveValue: { processedImage in self.displayImage(processedImage) } ) .store(in: &cancellables) } private func downloadImage(from url: URL) async throws -> Data { let (data, _) = try await URLSession.shared.data(from: url) return data } private func processImage(_ data: Data) async throws -> UIImage { // Async image processing } } ``` ### Publisher แบบกำหนดเองสำหรับสตรีม Async สำหรับความต้องการขั้นสูงกว่า Publisher แบบกำหนดเองสามารถรวบรวมสตรีม AsyncSequence ที่สมบูรณ์ได้ ```swift // AsyncSequencePublisher.swift // Publisher wrapper for AsyncSequence import Combine struct AsyncSequencePublisher: Publisher { typealias Output = S.Element typealias Failure = Error private let sequence: S init(_ sequence: S) { self.sequence = sequence } func receive(subscriber: Sub) where Sub: Subscriber, Failure == Sub.Failure, Output == Sub.Input { let subscription = AsyncSubscription( sequence: sequence, subscriber: subscriber ) subscriber.receive(subscription: subscription) } } private final class AsyncSubscription: Subscription where Sub.Input == S.Element, Sub.Failure == Error { private var task: Task? private var subscriber: Sub? private let sequence: S init(sequence: S, subscriber: Sub) { self.sequence = sequence self.subscriber = subscriber } func request(_ demand: Subscribers.Demand) { // Start asynchronous iteration task = Task { do { for try await element in sequence { // Check subscription is still active guard subscriber != nil else { break } _ = subscriber?.receive(element) } subscriber?.receive(completion: .finished) } catch { subscriber?.receive(completion: .failure(error)) } } } func cancel() { task?.cancel() subscriber = nil } } // Convenience extension for any AsyncSequence extension AsyncSequence { var publisher: AsyncSequencePublisher { AsyncSequencePublisher(self) } } ``` ## กลยุทธ์การอยู่ร่วมกันในโค้ดเบส การย้ายโค้ดเบสขนาดใหญ่ทั้งหมดต้องใช้เวลา ต่อไปนี้คือรูปแบบที่ทำให้ Combine และ async/await อยู่ร่วมกันอย่างกลมกลืน ### สถาปัตยกรรมแบบเป็นชั้นพร้อมการแยกนามธรรม การกำหนดโปรโตคอลที่แยกการนำไปใช้ออกจากกัน ช่วยให้สามารถย้ายแบบค่อยเป็นค่อยไปโดยไม่ต้องแก้ไขโค้ดที่เรียกใช้ ```swift // RepositoryAbstraction.swift // Abstraction enabling two implementations import Combine // Protocol defining the contract protocol UserRepositoryProtocol { // Modern async interface func fetchUser(id: String) async throws -> User // Legacy Combine interface (optional with default implementation) func fetchUserPublisher(id: String) -> AnyPublisher } // Default Publisher implementation based on async extension UserRepositoryProtocol { func fetchUserPublisher(id: String) -> AnyPublisher { Future { promise in Task { do { let user = try await self.fetchUser(id: id) promise(.success(user)) } catch { promise(.failure(error)) } } } .eraseToAnyPublisher() } } // Modern implementation - async first class UserRepository: UserRepositoryProtocol { private let apiClient: APIClient init(apiClient: APIClient) { self.apiClient = apiClient } func fetchUser(id: String) async throws -> User { // Native async implementation 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) } // fetchUserPublisher is provided by the default extension } ``` แนวทางนี้ช่วยให้ผู้เรียกใช้ใหม่สามารถใช้ async/await ได้ในขณะที่โค้ดเดิมยังคงใช้ Publisher > **ระวังเรื่องการจัดการหน่วยความจำ** > > ระหว่างการเชื่อมโยง `Task` ที่สร้างขึ้นอาจมีอายุยาวนานกว่าวัตถุที่สร้างมัน ควรใช้ `[weak self]` เสมอ หรือยกเลิกงานอย่างชัดเจนเพื่อหลีกเลี่ยงการรั่วไหลของหน่วยความจำ ### ViewModel แบบไฮบริด ViewModel สามารถเปิดเผยทั้งสองอินเทอร์เฟซในช่วงเปลี่ยนผ่าน ```swift // HybridViewModel.swift // ViewModel supporting both Combine and async/await import Combine import SwiftUI @MainActor class ProfileViewModel: ObservableObject { // Published state for SwiftUI (Combine) @Published private(set) var user: User? @Published private(set) var isLoading = false @Published private(set) var errorMessage: String? private let repository: UserRepositoryProtocol private var cancellables = Set() private var loadTask: Task? init(repository: UserRepositoryProtocol) { self.repository = repository } // Async interface for modern UIKit or SwiftUI with .task func loadUser(id: String) async { isLoading = true errorMessage = nil do { user = try await repository.fetchUser(id: id) } catch { errorMessage = error.localizedDescription } isLoading = false } // Combine interface for legacy code func loadUserPublisher(id: String) { isLoading = true errorMessage = nil repository.fetchUserPublisher(id: id) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } }, receiveValue: { [weak self] user in self?.user = user } ) .store(in: &cancellables) } // Clean cancellation func cancelLoading() { loadTask?.cancel() cancellables.removeAll() isLoading = false } } ``` ## การย้ายตัวดำเนินการ Combine ทั่วไป ตัวดำเนินการ Combine บางตัวไม่มีตัวเทียบเท่าโดยตรงใน async/await ต่อไปนี้คือวิธีสร้างขึ้นใหม่ ### ตัวเทียบเท่าของ Debounce ด้วย async ```swift // DebounceAsync.swift // Debounce implementation with async/await import Foundation actor Debouncer { private var task: Task? private let duration: Duration init(duration: Duration) { self.duration = duration } // Cancels previous execution and schedules a new one func debounce(_ operation: @escaping @Sendable () async -> Void) { task?.cancel() task = Task { do { // Wait for the specified duration try await Task.sleep(for: duration) // Execute operation if not cancelled await operation() } catch { // Task cancelled - expected behavior } } } } // Usage in a ViewModel @MainActor class SearchViewModel: ObservableObject { @Published var searchText = "" @Published private(set) var results: [SearchResult] = [] private let debouncer = Debouncer(duration: .milliseconds(300)) private let searchService: SearchService init(searchService: SearchService) { self.searchService = searchService } func onSearchTextChanged(_ text: String) { Task { await debouncer.debounce { [weak self] in guard let self else { return } await self.performSearch(text) } } } private func performSearch(_ query: String) async { guard query.count >= 3 else { results = [] return } do { results = try await searchService.search(query: query) } catch { // Error handling } } } ``` ### ตัวเทียบเท่าของ Merge ด้วย TaskGroup ```swift // MergeAsync.swift // Combining multiple async streams with TaskGroup import Foundation struct AsyncMerge { // Executes multiple async operations in parallel and returns all results static func merge( _ operations: [@Sendable () async throws -> T] ) async throws -> [T] { try await withThrowingTaskGroup(of: T.self) { group in // Launch all operations in parallel for operation in operations { group.addTask { try await operation() } } // Collect results var results: [T] = [] for try await result in group { results.append(result) } return results } } // Streaming version that emits results as they arrive static func mergeStream( _ operations: [@Sendable () async throws -> T] ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in Task { await withThrowingTaskGroup(of: T.self) { group in for operation in operations { group.addTask { try await operation() } } do { for try await result in group { continuation.yield(result) } continuation.finish() } catch { continuation.finish(throwing: error) } } } } } } // Usage class DataAggregator { func fetchAllData() async throws -> AggregatedData { // Execute three requests in parallel let results = try await AsyncMerge.merge([ { try await self.fetchUsers() }, { try await self.fetchPosts() }, { try await self.fetchComments() } ]) return AggregatedData( users: results[0] as! [User], posts: results[1] as! [Post], comments: results[2] as! [Comment] ) } } ``` ## กรณีการใช้งานที่ Combine ยังคงเป็นที่นิยม แม้จะมีข้อดีของ async/await แต่บางสถานการณ์ก็ยังคงเหมาะกับ Combine มากกว่า ### สตรีมเหตุการณ์ UI แบบรีแอ็กทีฟ SwiftUI และ UIKit สร้างสตรีมเหตุการณ์ที่ต่อเนื่อง ซึ่งตัวดำเนินการ Combine (debounce, throttle, combineLatest) เปล่งประกาย ```swift // UIEventsCombine.swift // Combine remains optimal for reactive UI events import Combine import SwiftUI class FormViewModel: ObservableObject { @Published var email = "" @Published var password = "" @Published var confirmPassword = "" // Derived states computed via Combine @Published private(set) var isEmailValid = false @Published private(set) var isPasswordStrong = false @Published private(set) var passwordsMatch = false @Published private(set) var canSubmit = false private var cancellables = Set() init() { setupValidation() } private func setupValidation() { // Email validation with debounce $email .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .map { email in let regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ return email.wholeMatch(of: regex) != nil } .assign(to: &$isEmailValid) // Password strength validation $password .map { password in password.count >= 8 && password.rangeOfCharacter(from: .uppercaseLetters) != nil && password.rangeOfCharacter(from: .decimalDigits) != nil } .assign(to: &$isPasswordStrong) // Password matching Publishers.CombineLatest($password, $confirmPassword) .map { password, confirm in !password.isEmpty && password == confirm } .assign(to: &$passwordsMatch) // Final combination to enable submit button Publishers.CombineLatest3($isEmailValid, $isPasswordStrong, $passwordsMatch) .map { $0 && $1 && $2 } .assign(to: &$canSubmit) } } ``` รูปแบบเชิงประกาศนี้จะมีความยืดยาวมากกว่ามากด้วย async/await ### การจัดการการเชื่อมต่อ WebSocket WebSocket ปล่อยข้อความอย่างต่อเนื่อง เป็นกรณีการใช้งานที่เป็นธรรมชาติสำหรับ Combine ```swift // WebSocketCombine.swift // WebSocket with Combine for continuous stream import Combine import Foundation class WebSocketManager: ObservableObject { @Published private(set) var messages: [ChatMessage] = [] @Published private(set) var connectionState: ConnectionState = .disconnected private var webSocketTask: URLSessionWebSocketTask? private let messageSubject = PassthroughSubject() private var cancellables = Set() // Exposed Publisher for consumers var messagePublisher: AnyPublisher { messageSubject.eraseToAnyPublisher() } func connect(to url: URL) { webSocketTask = URLSession.shared.webSocketTask(with: url) webSocketTask?.resume() connectionState = .connected // Start reception loop receiveMessages() // Message processing pipeline messageSubject // Buffer messages to avoid too frequent UI updates .collect(.byTime(RunLoop.main, .milliseconds(100))) // Accumulate in history .scan([ChatMessage]()) { accumulated, new in accumulated + new } .assign(to: &$messages) } private func receiveMessages() { webSocketTask?.receive { [weak self] result in switch result { case .success(let message): if case .string(let text) = message, let data = text.data(using: .utf8), let chatMessage = try? JSONDecoder().decode(ChatMessage.self, from: data) { self?.messageSubject.send(chatMessage) } // Continue reception self?.receiveMessages() case .failure(let error): self?.connectionState = .error(error.localizedDescription) } } } } ``` ## รายการตรวจสอบสำหรับการย้ายระบบแบบค่อยเป็นค่อยไป การย้ายระบบที่ประสบความสำเร็จใช้แนวทางที่เป็นระบบ ต่อไปนี้คือขั้นตอนที่แนะนำ ### ระยะที่ 1: การเตรียมการ - ✅ ระบุ Publisher ที่ใช้ในโค้ดเบส - ✅ จัดประเภท: สตรีมต่อเนื่อง vs การดำเนินการครั้งเดียว - ✅ สร้าง extension สำหรับการเชื่อมโยง (firstValue, asyncMap) - ✅ กำหนดโปรโตคอลแบบนามธรรมสำหรับ repository ### ระยะที่ 2: การย้ายการดำเนินการครั้งเดียว - ✅ แปลงคำขอเครือข่ายแบบง่ายเป็น async/await - ✅ ย้ายการอ่านไฟล์ - ✅ แปลงการดำเนินการกับฐานข้อมูล - ✅ รักษา Publisher ผ่านการนำไปใช้แบบเริ่มต้น ### ระยะที่ 3: การปรับ ViewModel - ✅ เพิ่มเมธอด async ให้กับ ViewModel ที่มีอยู่ - ✅ ใช้ `.task` ใน SwiftUI สำหรับหน้าจอใหม่ - ✅ รักษาการเชื่อมโยง @Published เพื่อความเข้ากันได้ ### ระยะที่ 4: การทำความสะอาด - ✅ ลบเมธอด Combine ที่กลายเป็นไม่จำเป็น - ✅ ลบ extension การเชื่อมโยงที่ไม่ใช้ - ✅ บันทึกรูปแบบ Combine ที่เก็บไว้โดยตั้งใจ ## บทสรุป การย้ายจาก Combine ไปยัง async/await แสดงถึงวิวัฒนาการที่เป็นธรรมชาติสำหรับโครงการ Swift สมัยใหม่ แนวทางแบบค่อยเป็นค่อยไปที่ใช้รูปแบบการเชื่อมโยงสองทิศทาง ช่วยให้นำข้อดีของ async/await มาใช้ได้โดยไม่เกิดการขาดตอนอย่างรุนแรง **ประเด็นสำคัญที่ต้องจดจำ:** - ✅ Combine และ async/await ตอบสนองความต้องการที่แตกต่างกัน - ✅ `.values` แปลง Publisher เป็น AsyncSequence - ✅ `Future` + `Task` รวบรวมโค้ด async ใน Publisher - ✅ โปรโตคอลแบบนามธรรมช่วยให้การอยู่ร่วมกันง่ายขึ้น - ✅ Combine ยังคงเหมาะสมสำหรับสตรีม UI แบบรีแอ็กทีฟ - ✅ ตัวดำเนินการเช่น debounce สามารถสร้างขึ้นใหม่ใน async ได้ - ✅ การย้ายแบบค่อยเป็นค่อยไปลดความเสี่ยงในการถดถอย เป้าหมายไม่ใช่การกำจัด Combine แต่เป็นการเลือกเครื่องมือที่เหมาะสมสำหรับแต่ละบริบท: async/await สำหรับการดำเนินการครั้งเดียว Combine สำหรับสตรีมเหตุการณ์ที่ต่อเนื่อง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/combine-vs-async-await-swift-progressive-migration-patterns