Combine คือเฟรมเวิร์กการเขียนโปรแกรมเชิงปฏิกิริยาแบบเนทีฟของ Apple ที่เปิดตัวใน iOS 13, macOS Catalina, tvOS 13 และ watchOS 6 โดยมี Swift API แบบประกาศสำหรับจัดการเหตุการณ์แบบอะซิงโครนัสผ่านรูปแบบ Publisher และ Subscriber แทนที่ delegates, closures และ NotificationCenter ด้วยเชนแบบรวมศูนย์ ตามข้อมูลของ Apple, 2025 Combine เป็นรากฐานสำหรับ SwiftUI และสถาปัตยกรรม iOS สมัยใหม่ ทำงานร่วมกับ async/await และ Structured Concurrency อย่างใกล้ชิด เฟรมเวิร์กถูกออกแบบมาเพื่อการประกอบการดำเนินการแบบอะซิงโครนัสพร้อมการรับประกันความปลอดภัยของเธรด
ประเด็นสำคัญ
Combine คือเฟรมเวิร์กการเขียนโปรแกรมเชิงปฏิกิริยาแบบประกาศที่อยู่ใน Apple SDK มันใช้รูปแบบ Reactive Streams: Publisher สร้างค่า Subscriber ใช้ค่าเหล่านั้น และตัวดำเนินการแปลงสตรีมระหว่างพวกมัน Combine แก้ปัญหา callbacks และ delegates โดยให้โมเดลการประกอบแบบรวมศูนย์สำหรับเหตุการณ์แบบอะซิงโครนัสใด ๆ — จากการตอบสนองของเครือข่ายไปจนถึงการเปลี่ยนแปลง UI
ก่อน Combine นักพัฒนา iOS ใช้ไลบรารีของบุคคลที่สาม โดยเฉพาะ RxSwift Apple สร้าง Combine เป็นทางเลือกแบบเนทีฟพร้อมการบูรณาการอย่างลึกซึ้งในระบบนิเวศ: เฟรมเวิร์กรองรับ Objective-C ผ่านบริดจ์ @objc ทำงานร่วมกับ KVO (Key-Value Observing ผ่าน NSObject.keyValuePublisher) และ NotificationCenter และยังเป็นพื้นฐานสำหรับ SwiftUI คอมโพเนนต์ UIKit ทั้งหมดที่เผยแพร่ใน SwiftUI ใช้ Combine ภายในสำหรับการอัปเดตมุมมอง
Combine ถูกออกแบบโดยคำนึงถึง Swift Concurrency: เริ่มจาก iOS 15, Publisher สามารถแปลงเป็น AsyncSequence ผ่าน .values และใช้ในลูป for-await-in การแปลงกลับจากฟังก์ชัน async เป็น Publisher ทำผ่าน Future ตามข้อมูลของ Apple WWDC 2024, Combine ยังคงเป็นเฟรมเวิร์กที่แนะนำสำหรับการจัดการข้อมูลสตรีมมิงในแอปพลิเคชัน UIKit แม้จะมีการนำ async/await มาใช้สำหรับการเรียกแบบอะซิงโครนัสครั้งเดียว
Combine ขึ้นอยู่กับสามโปรโตคอล: Publisher (ปล่อยค่าชนิด Output, สามารถล้มเหลวด้วยข้อผิดพลาดชนิด Failure), Subscriber (รับค่า, จัดการ Demand — จำนวนองค์ประกอบที่ร้องขอ), Subscription (แทนการเชื่อมต่อ Publisher-Subscriber ด้วยความสามารถในการยกเลิก) ช่องข้อมูลเริ่มต้นเมื่อ subscribe และสิ้นสุดเมื่อยกเลิก เสร็จสมบูรณ์ หรือมีข้อผิดพลาด Demand เป็นแนวคิดเฉพาะของ Combine: Subscriber บอก Publisher ว่าพร้อมที่จะประมวลผลกี่องค์ประกอบ โดยใช้ backpressure ในระดับโปรโตคอล
Publisher คือโปรโตคอลที่มีสองชนิดที่เกี่ยวข้อง: Output (ชนิดของค่าที่ปล่อย) และ Failure (ชนิดข้อผิดพลาดที่ใช้ Error) หากสตรีมไม่สามารถล้มเหลว Failure จะถูกระบุเป็น Never — สิ่งนี้รับประกันกับ Subscriber ว่า onReceive จะถูกเรียกด้วย Output เท่านั้น Publisher ในตัวประกอบด้วย Just (ค่าเดียว), Sequence (อาร์เรย์), URLSession.DataTaskPublisher (คำขอเครือข่าย), NotificationCenter.Publisher และ @Published property wrapper
import Combine
// การสร้าง Publisher จากลำดับ
let publisher = [1, 2, 3, 4, 5].publisher
// การสร้าง Subscriber พร้อมการจัดการค่า
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("เสร็จสมบูรณ์")
}
}
publisher.subscribe(PrintSubscriber())
Subscription คือโปรโตคอลที่แทนการเชื่อมต่อที่ใช้งานระหว่าง Publisher และ Subscriber Subscriber รับ Subscription ในเมธอด receive(subscription:) และเรียก request(_:) เพื่อระบุ Demand: .unlimited (ค่าทั้งหมด), .max(N) (จำนวนจำกัด) หรือ .none (หยุดชั่วคราว) Demand สามารถเปลี่ยนแปลงแบบไดนามิก — Subscriber สามารถเพิ่มหรือลดจำนวนองค์ประกอบที่ร้องขอระหว่างการรับข้อมูล สิ่งนี้ให้ backpressure โดยไม่ต้องบัฟเฟอร์ฝั่ง Publisher
Subject คือชนิดที่รวม Publisher และ Subscriber เข้าด้วยกัน Subject สามารถใช้เป็น Publisher (สมาชิกสมัครสมาชิก) และพร้อมกันเป็น Subscriber (ส่งค่าไปยังมัน) Combine มี Subject สองชนิด: PassthroughSubject (ไม่เก็บสถานะ, ส่งต่อเฉพาะค่าใหม่) และ CurrentValueSubject (เก็บค่าปัจจุบันและส่งต่อไปยังสมาชิกใหม่) Subject จำเป็นสำหรับการรวมโค้ดแบบ imperative ในเชนเชิงปฏิกิริยาของ Combine
let subject = PassthroughSubject<String, Never>()
// การสมัครสมาชิกเป็น Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// การส่งค่าเป็น Subscriber
subject.send("hello") // พิมพ์ "HELLO"
subject.send("world") // พิมพ์ "WORLD"
CurrentValueSubject แตกต่างจาก PassthroughSubject ตรงที่มีค่าเริ่มต้นและคุณสมบัติ value: สมาชิกได้รับค่าปัจจุบันทันทีเมื่อสมัครสมาชิก จากนั้นได้รับการอัปเดตที่ตามมาทั้งหมด CurrentValueSubject.value สามารถอ่านและเขียนได้ — การเปลี่ยน value จะส่งค่าใหม่ไปยังสมาชิกทั้งหมดโดยอัตโนมัติ สิ่งนี้ทำให้ CurrentValueSubject เป็นตัวเลือกที่เหมาะสมสำหรับการแทนสถานะในสถาปัตยกรรม MVVM: ViewModel เผยแพร่ CurrentValueSubject, View สมัครสมาชิกการเปลี่ยนแปลงผ่าน sink
ทั้งสอง Subject สามารถยุติสตรีมโดยเรียก send(completion: .finished) หรือ send(completion: .failure(error)) หลังจากการยุติ Subject หยุดรับและปล่อยเหตุการณ์ สำหรับสตรีมระยะยาวที่ไม่ควรยุติ (เช่น เหตุการณ์ UI) แนะนำให้ใช้ PassthroughSubject กับ Never Failure เพื่อหลีกเลี่ยงการเรียก send(completion:) โดยไม่ตั้งใจ
ตัวดำเนินการ Combine คือเมธอดของ Publisher ที่ส่งคืน Publisher ใหม่ แต่ละตัวดำเนินการสร้างออบเจกต์ใหม่ที่สมัครสมาชิก Publisher ต้นทางและปล่อยค่าที่แปลงแล้วปลายทาง เนื่องจาก Publisher เป็นชนิดทั่วไป ตัวดำเนินการรักษาการกำหนดชนิดที่เข้มงวด: map แปลง Output<A> เป็น Output<B>, tryMap เพิ่มความเป็นไปได้ของข้อผิดพลาด Combine มีตัวดำเนินการในตัวประมาณ 100 ตัว
| หมวดหมู่ | ตัวดำเนินการ | วัตถุประสงค์ |
|---|---|---|
| การแปลง | map / tryMap / flatMap | แปลงค่าหรือสตรีม |
| การกรอง | filter / compactMap / removeDuplicates | เลือกหรือทำความสะอาดค่า |
| การรวม | combineLatest / zip / merge | รวมหลาย Publisher |
| การควบคุมเวลา | debounce / throttle / delay | หน่วงและเว้นระยะเหตุการณ์ |
| การจัดการข้อผิดพลาด | catch / retry / replaceError | กู้คืนหลังจาก Failure |
| การจัดการ Demand | buffer / collect | การจัดกลุ่มหรือบัฟเฟอร์ |
flatMap ใน Combine มีความแตกต่างที่สำคัญจากเวอร์ชัน RxSwift: มันรับ closure ที่ส่งคืน Publisher ด้วยชนิด Failure เดียวกันและทำให้ Publisher ที่ซ้อนกันแบนลงในสตรีมหลัก flatMap กับ maxPublishers: .max(1) ทำตัวเหมือน switchMap — ยกเลิก Publisher ที่ซ้อนกันก่อนหน้าเมื่อมีค่าใหม่มาถึง สิ่งนี้สำคัญสำหรับสถานการณ์การค้นหา: เมื่อพิมพ์อักขระใหม่ คำขอ HTTP ก่อนหน้าจะถูกยกเลิกโดยอัตโนมัติ
// การค้นหาแบบ debounce พร้อมยกเลิกคำขอก่อนหน้า
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
ตัวดำเนินการรวม — combineLatest และ zip — ทำงานคล้ายกับ RxSwift: combineLatest ปล่อยทูเพิลของค่าล่าสุดจาก Publisher ทั้งหมดเมื่อมีการเปลี่ยนแปลงใด ๆ zip จับคู่ค่าตามดัชนี merge รวม Publisher ชนิดเดียวกันเป็นสตรีมเดียว ไม่รับประกันการรักษาลำดับ Combine ยังมี select — ตัวดำเนินการหายากที่เลือก Publisher ที่เสร็จสมบูรณ์ตัวแรกจากหลายตัว และ share — การกระจายสตรีมไปยังสมาชิกหลายคนโดยไม่ต้องดำเนินการซ้ำ
Scheduler ใน Combine คือโปรโตคอลที่กำหนดบริบทการดำเนินการสำหรับตัวดำเนินการ ต่างจาก RxSwift ที่มี Scheduler ในตัวมากกว่า 5 ตัว Combine ใช้กลไกที่มีอยู่ของ Apple: DispatchQueue, RunLoop และ OperationQueue แต่ละชนิดเหล่านี้สอดคล้องกับโปรโตคอล Scheduler ทำให้สามารถส่งต่อไปยัง receive(on:) และ subscribe(on:) โดยตรงโดยไม่ต้องใช้อะแดปเตอร์เพิ่มเติม
receive(on:) สลับปลายทางไปยัง Scheduler ที่ระบุ — เทียบเท่ากับ observeOn ใน RxSwift ตัวดำเนินการทั้งหมดหลังจาก receive(on:) ดำเนินการบน Scheduler ที่ระบุ subscribe(on:) สลับต้นทาง — มีผลต่อการดำเนินการของ Publisher รูปแบบทั่วไป: subscribe(on: DispatchQueue.global()) สำหรับงานพื้นหลังและ receive(on: DispatchQueue.main) สำหรับการอัปเดต UI ใน SwiftUI เมื่อใช้ .onReceive การเชื่อมโยงในตัวกับเธรดหลักไม่จำเป็น แต่สำหรับ sink แนะนำให้ใช้ receive(on:).main อย่างชัดเจน
// การโหลดพื้นหลัง + UI บนเธรดหลัก
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main เป็นทางเลือกของ DispatchQueue.main สำหรับการดำเนินการ UI ความแตกต่างคือ RunLoop.main ผูกกับลูปเหตุการณ์ปัจจุบันของแอปพลิเคชัน ในขณะที่ DispatchQueue.main ผูกกับคิวเธรดหลักทั่วโลก สำหรับ UIKit แนะนำ DispatchQueue.main สำหรับ SwiftUI — RunLoop.main ImmediateWhenScheduler ดำเนินการแบบซิงโครนัสบนเธรดปัจจุบัน — ใช้โดยค่าเริ่มต้นสำหรับการทดสอบและ Publisher ธรรมดา
ObservableObject คือโปรโตคอล SwiftUI สำหรับออบเจกต์ที่เผยแพร่การเปลี่ยนแปลง คลาสที่ใช้ ObservableObject สามารถใช้ @Published property wrapper สำหรับคุณสมบัติที่การเปลี่ยนแปลงแจ้งให้ SwiftUI ทราบโดยอัตโนมัติเกี่ยวกับความจำเป็นในการวาดใหม่ ภายใต้ฝาครอบ @Published สร้าง Publisher ที่แจ้ง Publisher objectWillChange เมื่อ wrappedValue เปลี่ยนแปลง SwiftUI สมัครสมาชิก objectWillChange ผ่าน @StateObject, @ObservedObject หรือ @EnvironmentObject
@Published เป็นวิธีที่พบบ่อยที่สุดในการรวม Combine ใน SwiftUI เมื่อค่าของคุณสมบัติ @Published เปลี่ยนแปลง SwiftUI อัปเดต Views ทั้งหมดที่ใช้ออบเจกต์นั้น @StateObject สร้างอินสแตนซ์ ObservableObject และสมัครสมาชิกการเปลี่ยนแปลงของมัน View ที่สร้างด้วย @StateObject จะวาดใหม่โดยอัตโนมัติเมื่อคุณสมบัติ @Published เปลี่ยนแปลง หากออบเจกต์ต้องถูกแชร์ระหว่างหลาย Views จะใช้ @ObservedObject หรือ @EnvironmentObject
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable คือตัวห่อชนิดที่ลบสำหรับ Cancellable ที่เก็บโทเคนการยกเลิกการสมัครสมาชิก Set<AnyCancellable> จัดการวงจรชีวิตของการสมัครสมาชิก: เมื่อเจ้าของถูกดีอินิเชียลไลซ์ Cancellable ทั้งหมดจะถูกยกเลิกโดยอัตโนมัติ ในโปรเจกต์ SwiftUI ประกาศ Set<AnyCancellable> ในคลาส ObservableObject และเพิ่มการสมัครสมาชิกผ่าน .store(in: &cancellables) สำหรับ UIKit ใช้กลไกเดียวกันกับการจัดเก็บใน UIViewController ผ่าน &cancellables หรือการเรียก cancel() ด้วยตนเอง
Combine และ RxSwift แก้ปัญหาเดียวกันของการเขียนโปรแกรมเชิงปฏิกิริยาแต่มีความแตกต่างทางสถาปัตยกรรมพื้นฐาน Combine เป็นส่วนหนึ่งของ Apple SDK ที่มีความเข้ากันได้ย้อนหลังถึง iOS 13 RxSwift เป็นไลบรารีของบุคคลที่สามที่รองรับ iOS 8+ Combine ใช้การกำหนดชนิดข้อผิดพลาดที่เข้มงวดผ่าน Failure ทั่วไป RxSwift ใช้ชนิด Error เดียว Combine บูรณาการกับ SwiftUI ในระดับแพลตฟอร์ม RxSwift ต้องใช้ RxCocoa สำหรับส่วนขยาย UI
การเลือกระหว่าง Combine และ RxSwift ขึ้นอยู่กับข้อกำหนดของโปรเจกต์ หากเวอร์ชัน iOS ขั้นต่ำ >= 13 และโปรเจกต์ใช้ SwiftUI — Combine เป็นตัวเลือกธรรมชาติเนื่องจากการบูรณาการในตัวและไม่มี dependencies เพิ่มเติม หากโปรเจกต์รองรับ iOS 11-12 มีโค้ดเบส RxSwift ที่มีอยู่ หรือต้องการตัวดำเนินการเฉพาะที่มีเฉพาะใน RxSwift (เช่น Observable.from(path:)) — RxSwift ยังคงเป็นโซลูชันที่ถูกต้อง
| ลักษณะ | Combine | RxSwift |
|---|---|---|
| ผู้พัฒนา | Apple (ในตัวใน SDK) | ReactiveX (ชุมชน) |
| เวอร์ชัน iOS | iOS 13+ | iOS 8+ |
| ชนิดข้อผิดพลาด | Failure ทั่วไป (Never สำหรับ UI) | Error (ใด ๆ) |
| การบูรณาการ UI | @Published + SwiftUI | RxCocoa + UIKit |
| ตัวดำเนินการ | ~100 ในตัว | 400+ ตัวดำเนินการ |
| Swift Concurrency | ผ่าน .values (ลำดับแบบ async) | ผ่านไลบรารีบริดจ์ |
คำถามที่พบบ่อย
PassthroughSubject ไม่เก็บสถานะ — สมาชิกได้รับเฉพาะเหตุการณ์ที่ส่งหลังจากการสมัครสมาชิก CurrentValueSubject เก็บค่าปัจจุบันและส่งต่อไปยังสมาชิกใหม่แต่ละคนทันทีเมื่อสมัครสมาชิก CurrentValueSubject เหมาะสำหรับการแทนสถานะ (เช่น isLoggedIn)
การสมัครสมาชิกส่งคืน AnyCancellable ซึ่งจะถูกยกเลิกเมื่อเรียก cancel() หรือเมื่อดีอินิเชียลไลซ์ สำหรับการจัดการกลุ่มให้ใช้ Set<AnyCancellable> — การสมัครสมาชิกทั้งหมดจะถูกยกเลิกเมื่อล้างเซต สิ่งนี้คล้ายกับ DisposeBag ใน RxSwift
ใช่ Combine ยังคงสำคัญสำหรับข้อมูลสตรีมมิง: เหตุการณ์ UI, debounce, combineLatest, WebSocket async/await สะดวกสำหรับคำขอครั้งเดียว Combine — สำหรับสตรีมต่อเนื่องหรือหลายสตรีม เฟรมเวิร์กทั้งสองเสริมกัน — Publisher สามารถแปลงเป็น AsyncSequence
UIKit ไม่มี Publisher ในตัว แต่ Apple มีส่วนขยาย: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher สำหรับเหตุการณ์ UI ที่กำหนดเอง ใช้ PassthroughSubject หรือ @IBAction ที่ห่อใน Publisher ผ่าน Future หรือ Subject
Backpressure คือกลไกในการควบคุมความเร็วของสตรีม: Subscriber บอก Publisher ผ่าน Demand ว่าพร้อมที่จะประมวลผลกี่องค์ประกอบ หาก Demand = .max(1) Publisher จะรอคำขอก่อนส่งค่าถัดไป สิ่งนี้ป้องกันบัฟเฟอร์ล้นเมื่อความเร็วของผู้ผลิตและผู้บริโภคไม่ตรงกัน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ