# Combine Framework: การเขียนโปรแกรมเชิงรีแอกทีฟใน Swift > เชี่ยวชาญ Combine สำหรับการจัดการสตรีมข้อมูลแบบ asynchronous ใน Swift: Publishers, Subscribers, Operators และรูปแบบขั้นสูงสำหรับแอป iOS - Published: 2026-02-18 - Updated: 2026-04-28 - Author: SharpSkill - Tags: combine, swift, ios, reactive, async - Reading time: 14 min --- การเขียนโปรแกรมเชิงรีแอกทีฟเปลี่ยนวิธีการจัดการเหตุการณ์แบบ asynchronous และสตรีมข้อมูลในแอป iOS อย่างสิ้นเชิง Combine ซึ่งเป็นเฟรมเวิร์กดั้งเดิมของ Apple นำเสนอแนวทางแบบ declarative และปลอดภัยทางชนิดข้อมูล เพื่อจัดวางไปป์ไลน์ข้อมูลที่ซับซ้อน คู่มือนี้พาผู้อ่านจากแนวคิดพื้นฐานไปจนถึงรูปแบบที่พร้อมใช้งานจริง > **ทำไมถึงใช้ Combine แทน RxSwift?** > > Combine ติดตั้งมาพร้อมกับ iOS 13+ ให้ประสิทธิภาพดีกว่าด้วยการปรับแต่งของ Apple และเชื่อมต่อกับ SwiftUI ได้อย่างไร้รอยต่อ ไม่ต้องดูแล dependency ภายนอก ## แนวคิดหลักของ Combine Combine สร้างขึ้นบนแนวคิดสำคัญสามอย่าง: **Publishers** ที่ส่งค่า, **Subscribers** ที่รับค่า และ **Operators** ที่แปลงข้อมูลระหว่างทั้งสอง สถาปัตยกรรมนี้ช่วยให้สร้างไปป์ไลน์ข้อมูลแบบรีแอกทีฟที่ประกอบเข้าด้วยกันได้ ### Publisher: แหล่งข้อมูล Publisher คือชนิดข้อมูลที่สามารถส่งลำดับของค่าออกมาตามเวลาได้ Publisher แต่ละตัวจะประกาศชนิดที่เกี่ยวข้องสองชนิด: ชนิดของค่าที่ส่งออก (`Output`) และชนิดของข้อผิดพลาดที่อาจเกิดขึ้น (`Failure`) ต่อไปนี้คือวิธีสร้าง Publisher แต่ละประเภท: ```swift // PublisherBasics.swift import Combine // Just: emits a single value then completes // Useful for converting a simple value to a Publisher let singleValue = Just("Hello Combine") // CurrentValueSubject: stores and emits the current value // Perfect for representing state that changes over time let counter = CurrentValueSubject(0) // PassthroughSubject: emits values without storing them // Ideal for one-time events (taps, notifications) let buttonTaps = PassthroughSubject() // Future: emits a single value asynchronously // Wraps an async operation that returns a result let asyncOperation = Future { promise in // Simulate a network call DispatchQueue.global().asyncAfter(deadline: .now() + 1) { promise(.success("Data loaded")) } } ``` ชนิด `Never` สำหรับ error หมายความว่า Publisher จะไม่มีวันล้มเหลว ซึ่งเป็นการรับประกัน ณ ตอนคอมไพล์ที่ช่วยทำให้โค้ดจัดการ error เรียบง่ายขึ้น ### Subscriber: รับค่า Subscriber คือผู้สมัครรับค่าจาก Publisher วิธีที่นิยมที่สุดในการสร้าง Subscriber คือเมธอด `sink` ซึ่งรับ closure สองตัว: ตัวหนึ่งสำหรับ error หรือการสิ้นสุด และอีกตัวสำหรับค่าทุกค่าที่รับเข้ามา: ```swift // SubscriberBasics.swift import Combine // Variable to store subscriptions // Without this reference, the subscription would be immediately cancelled var cancellables = Set() let publisher = ["Swift", "Combine", "iOS"].publisher // sink() creates a Subscriber that receives values publisher .sink( // Called when the Publisher completes or fails receiveCompletion: { completion in switch completion { case .finished: print("✅ Completed successfully") case .failure(let error): print("❌ Error: \(error)") } }, // Called for each emitted value receiveValue: { value in print("Received: \(value)") } ) // store() keeps a reference to the subscription .store(in: &cancellables) // Output: // Received: Swift // Received: Combine // Received: iOS // ✅ Completed successfully ``` > **ระวังการรั่วไหลของหน่วยความจำ** > > ต้องเก็บ `AnyCancellable` ที่ `sink()` คืนกลับมาเสมอ หากไม่มีการอ้างอิง การสมัครจะถูกยกเลิกทันทีและจะไม่ได้รับค่าใด ๆ ## การแปลงข้อมูลด้วย Operators Operators คือหัวใจของ Combine ทำให้สามารถแปลง กรอง และรวมสตรีมข้อมูลได้แบบ declarative Operator แต่ละตัวคืน Publisher ใหม่ออกมา ทำให้สามารถนำมาต่อเชื่อมเป็นลำดับได้ ### Operators แปลงข้อมูลที่จำเป็น Operators แปลงข้อมูลจะปรับเปลี่ยนค่าทุกค่าที่ถูกส่งออก `map` แปลงค่า, `flatMap` แผ่ Publisher ที่ซ้อนกัน และ `compactMap` กรองค่า nil ออก: ```swift // TransformOperators.swift import Combine var cancellables = Set() // map: transforms each value // Equivalent to map on arrays [1, 2, 3, 4, 5].publisher .map { $0 * 2 } // Multiply each number by 2 .sink { print("Doubled: \($0)") } .store(in: &cancellables) // Output: 2, 4, 6, 8, 10 // compactMap: transforms AND filters out nil // Useful for optional conversions ["1", "two", "3", "four", "5"].publisher .compactMap { Int($0) } // Convert to Int, ignore failures .sink { print("Valid number: \($0)") } .store(in: &cancellables) // Output: 1, 3, 5 // flatMap: flattens nested Publishers // Essential for chaining async operations struct User { let id: Int; let name: String } func fetchUser(id: Int) -> AnyPublisher { // Simulate an API call Just(User(id: id, name: "User \(id)")) .delay(for: .milliseconds(100), scheduler: RunLoop.main) .eraseToAnyPublisher() } [1, 2, 3].publisher .flatMap { id in fetchUser(id: id) } // Each ID becomes an API call .sink { user in print("User: \(user.name)") } .store(in: &cancellables) ``` ### Operators กรองข้อมูล Operators กรองข้อมูลควบคุมว่าค่าใดจะผ่านเข้าไปในไปป์ไลน์ จึงสำคัญต่อการเลี่ยงการประมวลผลที่ไม่จำเป็นและเพิ่มประสิทธิภาพ: ```swift // FilterOperators.swift import Combine var cancellables = Set() let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher // filter: keeps only values that satisfy the condition numbers .filter { $0 > 2 } // Keep only numbers > 2 .sink { print("Filtered: \($0)") } .store(in: &cancellables) // Output: 3, 3, 3, 4, 5, 5 // removeDuplicates: removes consecutive identical values numbers .removeDuplicates() // Eliminate consecutive duplicates .sink { print("Without duplicates: \($0)") } .store(in: &cancellables) // Output: 1, 2, 3, 4, 5 // debounce: waits for a pause before emitting // Perfect for real-time search let searchText = PassthroughSubject() searchText .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() // Ignore if text hasn't changed .sink { query in print("Search: \(query)") // Launch API call here } .store(in: &cancellables) // Simulate rapid typing searchText.send("S") searchText.send("Sw") searchText.send("Swi") searchText.send("Swift") // Only "Swift" is emitted after 300ms ``` ## การรวม Publisher หลายตัว แอปพลิเคชันในโลกจริงมักต้องรวมแหล่งข้อมูลหลายแหล่งเข้าด้วยกัน Combine มี Operators หลายตัวสำหรับจัดวางสตรีมหลายเส้นทางเหล่านี้ ### CombineLatest และ Zip `combineLatest` จะส่งค่าออกทุกครั้งที่ Publisher ใด ๆ ส่งค่าออกมา โดยรวมกับค่าใหม่ล่าสุดของตัวอื่น ส่วน `zip` รอจน Publisher ทุกตัวส่งค่าออกมาก่อนจึงจะรวมค่ากัน: ```swift // CombiningPublishers.swift import Combine var cancellables = Set() // Simulate a form with validation let email = CurrentValueSubject("") let password = CurrentValueSubject("") // combineLatest: combines the latest values from each Publisher // Emits on every change from either source Publishers.CombineLatest(email, password) .map { email, password in // Validate that email contains @ and password > 6 chars let isEmailValid = email.contains("@") let isPasswordValid = password.count >= 6 return isEmailValid && isPasswordValid } .sink { isFormValid in print("Form valid: \(isFormValid)") } .store(in: &cancellables) email.send("user@example.com") // false (password empty) password.send("123456") // true (both are valid) // zip: waits for one value from each Publisher before emitting // Useful for synchronizing parallel operations let firstAPI = PassthroughSubject() let secondAPI = PassthroughSubject() Publishers.Zip(firstAPI, secondAPI) .sink { stringValue, intValue in print("Received pair: \(stringValue), \(intValue)") } .store(in: &cancellables) firstAPI.send("Hello") // No emission, waiting for secondAPI secondAPI.send(42) // Emits: ("Hello", 42) firstAPI.send("World") // No emission, waiting for secondAPI secondAPI.send(100) // Emits: ("World", 100) ``` ### Merge เพื่อรวมสตรีมเป็นหนึ่งเดียว `merge` รวม Publisher ชนิดเดียวกันหลายตัวให้เป็นสตรีมเดียว ค่ามาถึงตามลำดับการส่งโดยไม่สนใจว่ามาจาก Publisher ตัวใด: ```swift // MergePublishers.swift import Combine var cancellables = Set() // Multiple user notification sources let pushNotifications = PassthroughSubject() let localNotifications = PassthroughSubject() let inAppMessages = PassthroughSubject() // Merge unifies all streams into one Publishers.Merge3(pushNotifications, localNotifications, inAppMessages) .sink { message in // Handle all notifications the same way print("📬 Notification: \(message)") } .store(in: &cancellables) pushNotifications.send("New message") // 📬 Notification: New message localNotifications.send("Reminder: meeting") // 📬 Notification: Reminder: meeting inAppMessages.send("Welcome!") // 📬 Notification: Welcome! ``` ## การจัดการข้อผิดพลาดใน Combine การจัดการข้อผิดพลาดถูกฝังไว้ในแกนกลางของ Combine ชนิด `Failure` ของ Publisher ช่วยให้คอมไพเลอร์ตรวจสอบได้ว่าทุก error ถูกจัดการแล้ว ### กลยุทธ์การกู้คืน Combine มี Operators หลายตัวสำหรับจัดการ error: `catch` เพื่อแทนที่ด้วย Publisher อื่น, `retry` เพื่อพยายามอีกครั้ง และ `replaceError` สำหรับค่าตั้งต้น: ```swift // ErrorHandling.swift import Combine var cancellables = Set() enum APIError: Error { case networkError case invalidResponse case serverError(Int) } // Simulate an API call that can fail func fetchData() -> AnyPublisher { Fail(error: APIError.networkError) .eraseToAnyPublisher() } // retry: retries N times before propagating the error fetchData() .retry(3) // Try up to 3 times .catch { error -> Just in // catch: replaces the error with a fallback Publisher print("Error after 3 attempts: \(error)") return Just("Cached data") // Fallback value } .sink( receiveCompletion: { _ in }, receiveValue: { print("Result: \($0)") } ) .store(in: &cancellables) // replaceError: replaces any error with a fixed value // Simpler than catch when only a default value is needed fetchData() .replaceError(with: "Error - default value") .sink { print("With fallback: \($0)") } .store(in: &cancellables) ``` > **Never กับ Error** > > ใช้ `setFailureType(to:)` เพื่อแปลง Publisher ชนิด `Never` ให้กลายเป็นชนิดที่ล้มเหลวได้ และใช้ `replaceError(with:)` หรือ `catch` สำหรับทิศทางตรงกันข้าม ## รูปแบบ MVVM กับ Combine Combine ผสานเข้ากับรูปแบบ MVVM (Model-View-ViewModel) ได้อย่างเป็นธรรมชาติ ViewModel เปิดเผย Publisher ที่ View คอยสังเกต ทำให้เกิดการ binding แบบรีแอกทีฟระหว่างข้อมูลกับอินเทอร์เฟซ ### ViewModel แบบรีแอกทีฟครบถ้วน นี่คือตัวอย่าง ViewModel สำหรับรายการผู้ใช้ที่มีการค้นหา การโหลด และการจัดการ error: ```swift // UserListViewModel.swift import Combine import Foundation // Data model struct User: Codable, Identifiable { let id: Int let name: String let email: String } // ViewModel with reactive state final class UserListViewModel: ObservableObject { // MARK: - Published Properties (observed by SwiftUI) @Published var users: [User] = [] // User list @Published var searchQuery: String = "" // Search text @Published var isLoading: Bool = false // Loading state @Published var errorMessage: String? // Optional error message // MARK: - Private Properties private var cancellables = Set() private let userService: UserServiceProtocol // MARK: - Computed Properties // Filters users based on search query var filteredUsers: [User] { guard !searchQuery.isEmpty else { return users } return users.filter { $0.name.localizedCaseInsensitiveContains(searchQuery) } } // MARK: - Initialization init(userService: UserServiceProtocol = UserService()) { self.userService = userService setupBindings() } // MARK: - Private Methods private func setupBindings() { // Observe searchQuery changes // debounce prevents too frequent calls $searchQuery .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] query in // Server-side search logic if needed print("Search updated: \(query)") } .store(in: &cancellables) } // MARK: - Public Methods func loadUsers() { isLoading = true errorMessage = nil userService.fetchUsers() .receive(on: DispatchQueue.main) // Ensure UI updates on main thread .sink( receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } }, receiveValue: { [weak self] users in self?.users = users } ) .store(in: &cancellables) } } ``` ### Service ด้วย Combine และ URLSession URLSession ผนวก Combine ไว้แบบเนทีฟผ่าน `dataTaskPublisher` นี่คือวิธีสร้างเซอร์วิสเครือข่ายที่นำกลับมาใช้ได้: ```swift // UserService.swift import Combine import Foundation protocol UserServiceProtocol { func fetchUsers() -> AnyPublisher<[User], Error> } final class UserService: UserServiceProtocol { private let baseURL = URL(string: "https://api.example.com")! private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared) { self.session = session self.decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase } func fetchUsers() -> AnyPublisher<[User], Error> { let url = baseURL.appendingPathComponent("users") return session.dataTaskPublisher(for: url) // Check HTTP status code .tryMap { data, response in guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard 200..<300 ~= httpResponse.statusCode else { throw URLError(.badServerResponse) } return data } // Decode JSON to Swift model .decode(type: [User].self, decoder: decoder) // Erase concrete type to return AnyPublisher .eraseToAnyPublisher() } } ``` ## การทำงานร่วมกับ SwiftUI Combine กับ SwiftUI เป็นคู่หูที่ทรงพลัง คุณสมบัติ `@Published` ของ `ObservableObject` จะกระตุ้นการอัปเดต view โดยอัตโนมัติ ### View ของ SwiftUI กับ ViewModel ของ Combine นี่คือวิธีเชื่อม ViewModel เข้ากับ view ของ SwiftUI: ```swift // UserListView.swift import SwiftUI struct UserListView: View { // StateObject: creates and owns the ViewModel @StateObject private var viewModel = UserListViewModel() var body: some View { NavigationStack { Group { if viewModel.isLoading { // Centered loading indicator ProgressView("Loading...") } else if let error = viewModel.errorMessage { // Error view with retry button VStack(spacing: 16) { Text("Error: \(error)") .foregroundStyle(.red) Button("Retry") { viewModel.loadUsers() } } } else { // User list List(viewModel.filteredUsers) { user in UserRowView(user: user) } } } .navigationTitle("Users") .searchable(text: $viewModel.searchQuery) // Direct binding .onAppear { viewModel.loadUsers() // Load on first appearance } } } } struct UserRowView: View { let user: User var body: some View { VStack(alignment: .leading, spacing: 4) { Text(user.name) .font(.headline) Text(user.email) .font(.subheadline) .foregroundStyle(.secondary) } .padding(.vertical, 4) } } ``` ## รูปแบบขั้นสูง ### การยกเลิกและการทำความสะอาดอัตโนมัติ การจัดการวงจรชีวิตของการสมัครเป็นสิ่งสำคัญเพื่อหลีกเลี่ยง memory leak รูปแบบ `cancellables` ที่ใช้ร่วมกับ `AnyCancellable` รับประกันการทำความสะอาดอัตโนมัติ: ```swift // CancellationPatterns.swift import Combine final class DataManager { // Set of cancellables: automatically cancelled on destruction private var cancellables = Set() // Individual cancellable for fine-grained control private var currentRequest: AnyCancellable? func startPolling() { // Timer that emits every 5 seconds Timer.publish(every: 5, on: .main, in: .common) .autoconnect() // Starts automatically .sink { [weak self] _ in self?.fetchLatestData() } .store(in: &cancellables) } func fetchLatestData() { // Cancel the previous request if it exists currentRequest?.cancel() currentRequest = URLSession.shared .dataTaskPublisher(for: URL(string: "https://api.example.com/data")!) .map(\.data) .decode(type: [String].self, decoder: JSONDecoder()) .replaceError(with: []) .receive(on: DispatchQueue.main) .sink { data in print("Data received: \(data)") } } deinit { // All cancellables are automatically cancelled print("DataManager destroyed, subscriptions cancelled") } } ``` ### Schedulers สำหรับ threading Schedulers ควบคุมว่าการดำเนินการจะถูกรันบน thread ใด ใช้ `subscribe(on:)` สำหรับงานเบื้องหลังและ `receive(on:)` สำหรับการอัปเดต UI: ```swift // SchedulerPatterns.swift import Combine import Foundation var cancellables = Set() func loadAndProcessData() -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: apiURL) // Perform parsing on a background thread .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .map(\.data) .decode(type: RawData.self, decoder: JSONDecoder()) // Heavy processing on background thread .map { rawData in // This expensive operation runs in the background processData(rawData) } // Return to main thread for UI .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } ``` ## บทสรุป Combine มอบแนวทางที่ทรงพลังและเป็นแบบ declarative ในการจัดการสตรีมข้อมูล asynchronous ในแอป iOS ประเด็นสำคัญ: ✅ **Publishers** ส่งค่าออกมาตามเวลา ✅ **Subscribers** รับและประมวลผลค่าเหล่านั้น ✅ **Operators** แปลงและรวมสตรีมเข้าด้วยกัน ✅ `AnyCancellable` จัดการวงจรชีวิตของการสมัคร ✅ `@Published` กับ SwiftUI สร้างการ binding แบบรีแอกทีฟอัตโนมัติ ✅ **Schedulers** ควบคุม threading เพื่อให้ได้ประสิทธิภาพสูงสุด การเชี่ยวชาญ Combine เปิดทางสู่การสร้างแอป iOS ที่แข็งแกร่ง ดูแลรักษาง่าย และตอบสนอง การผสานรวมเข้ากับ SwiftUI ในระดับเนทีฟทำให้ Combine กลายเป็นเครื่องมือสำคัญสำหรับการพัฒนา iOS สมัยใหม่ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/combine-reactive-programming-swift