Combine vs async/await ใน Swift: รูปแบบการย้ายระบบแบบค่อยเป็นค่อยไป
คู่มือฉบับสมบูรณ์สำหรับการย้ายจาก Combine ไปยัง async/await ใน Swift: กลยุทธ์แบบค่อยเป็นค่อยไป รูปแบบการเชื่อมโยง และการอยู่ร่วมกันของกระบวนทัศน์ในโค้ดเบส iOS

การมาถึงของ Swift Concurrency พร้อมกับ async/await ได้เปลี่ยนแปลงแนวปฏิบัติการเขียนโปรแกรมแบบไม่พร้อมกันบน iOS สำหรับโครงการที่ใช้ Combine คำถามเรื่องการย้ายระบบเกิดขึ้นโดยธรรมชาติ จำเป็นต้องเขียนใหม่ทั้งหมดหรือไม่ สองแนวทางสามารถอยู่ร่วมกันได้หรือไม่ รูปแบบใดที่ช่วยให้การเปลี่ยนผ่านราบรื่น คู่มือนี้สำรวจกลยุทธ์การย้ายระบบแบบค่อยเป็นค่อยไป ที่ช่วยให้นำ async/await มาใช้ได้โดยไม่ละทิ้ง Combine อย่างกะทันหัน
คู่มือนี้นำเสนอรูปแบบที่เป็นรูปธรรมสำหรับการย้ายระบบแบบค่อยเป็นค่อยไปจาก Combine ไปยัง async/await พร้อมตัวอย่างการเชื่อมโยงสองทิศทางและกลยุทธ์การอยู่ร่วมกันที่เหมาะสมกับโค้ดเบสที่มีอยู่
ทำความเข้าใจความแตกต่างพื้นฐาน
ก่อนเริ่มการย้ายระบบ การทำความเข้าใจสิ่งที่แยก Combine ออกจาก async/await เป็นเรื่องสำคัญ สองแนวทางนี้ตอบสนองความต้องการที่แตกต่างกัน และกรณีการใช้งานบางอย่างยังคงเหมาะกับ Combine มากกว่า
แบบจำลองทางความคิดของ Combine
Combine อิงจากแบบจำลองสตรีมข้อมูล Publisher ปล่อยค่าออกมาตามเวลา ตัวดำเนินการแปลงค่าเหล่านั้น และ Subscriber รับผลลัพธ์สุดท้าย แบบจำลองนี้โดดเด่นสำหรับสตรีมต่อเนื่อง เช่น เหตุการณ์ UI การแจ้งเตือน หรือ WebSocket
// Event stream with Combine - stream-based model
import Combine
class SearchViewModel {
@Published var searchText = ""
private var cancellables = Set<AnyCancellable>()
// 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 ใช้แบบจำลองตามลำดับ: การดำเนินการเริ่มต้น โค้ดรอผลลัพธ์ จากนั้นดำเนินการต่อ แบบจำลองนี้เป็นธรรมชาติมากกว่าสำหรับการดำเนินการครั้งเดียว เช่น คำขอเครือข่ายที่แยกออก หรือการอ่านไฟล์
// 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
// Publisher → AsyncSequence conversion via .values
import Combine
class NotificationObserver {
private let notificationPublisher: AnyPublisher<Notification, Never>
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 แบบกำหนดเองช่วยลดความซับซ้อนของการเชื่อมโยง
// 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 ที่สะอาด
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การเชื่อมโยงจาก async/await ไปยัง Combine
การย้ายในทิศทางตรงกันข้ามก็จำเป็นเช่นกัน: การบริโภคโค้ด async ในไปป์ไลน์ Combine ที่มีอยู่
การสร้าง Publisher จากฟังก์ชัน async
แนวทางที่ตรงที่สุดใช้ Future ผสมกับ Task เพื่อรวบรวมการเรียกใช้ async
// async → Publisher conversion via Future
import Combine
extension Publisher {
// async flatMap operator for Combine pipelines
func asyncMap<T>(
_ transform: @escaping (Output) async throws -> T
) -> AnyPublisher<T, Error> {
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<AnyCancellable>()
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 ที่สมบูรณ์ได้
// Publisher wrapper for AsyncSequence
import Combine
struct AsyncSequencePublisher<S: AsyncSequence>: Publisher {
typealias Output = S.Element
typealias Failure = Error
private let sequence: S
init(_ sequence: S) {
self.sequence = sequence
}
func receive<Sub>(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<S: AsyncSequence, Sub: Subscriber>: Subscription
where Sub.Input == S.Element, Sub.Failure == Error {
private var task: Task<Void, Never>?
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<Self> {
AsyncSequencePublisher(self)
}
}กลยุทธ์การอยู่ร่วมกันในโค้ดเบส
การย้ายโค้ดเบสขนาดใหญ่ทั้งหมดต้องใช้เวลา ต่อไปนี้คือรูปแบบที่ทำให้ Combine และ async/await อยู่ร่วมกันอย่างกลมกลืน
สถาปัตยกรรมแบบเป็นชั้นพร้อมการแยกนามธรรม
การกำหนดโปรโตคอลที่แยกการนำไปใช้ออกจากกัน ช่วยให้สามารถย้ายแบบค่อยเป็นค่อยไปโดยไม่ต้องแก้ไขโค้ดที่เรียกใช้
// 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<User, Error>
}
// Default Publisher implementation based on async
extension UserRepositoryProtocol {
func fetchUserPublisher(id: String) -> AnyPublisher<User, Error> {
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 สามารถเปิดเผยทั้งสองอินเทอร์เฟซในช่วงเปลี่ยนผ่าน
// 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<AnyCancellable>()
private var loadTask: Task<Void, Never>?
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
// Debounce implementation with async/await
import Foundation
actor Debouncer {
private var task: Task<Void, Never>?
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
// Combining multiple async streams with TaskGroup
import Foundation
struct AsyncMerge {
// Executes multiple async operations in parallel and returns all results
static func merge<T>(
_ 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<T: Sendable>(
_ operations: [@Sendable () async throws -> T]
) -> AsyncThrowingStream<T, Error> {
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]
)
}
}พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
กรณีการใช้งานที่ Combine ยังคงเป็นที่นิยม
แม้จะมีข้อดีของ async/await แต่บางสถานการณ์ก็ยังคงเหมาะกับ Combine มากกว่า
สตรีมเหตุการณ์ UI แบบรีแอ็กทีฟ
SwiftUI และ UIKit สร้างสตรีมเหตุการณ์ที่ต่อเนื่อง ซึ่งตัวดำเนินการ Combine (debounce, throttle, combineLatest) เปล่งประกาย
// 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<AnyCancellable>()
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
// 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<ChatMessage, Never>()
private var cancellables = Set<AnyCancellable>()
// Exposed Publisher for consumers
var messagePublisher: AnyPublisher<ChatMessage, Never> {
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 สำหรับสตรีมเหตุการณ์ที่ต่อเนื่อง
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

การย้ายจาก Core Data ไปยัง SwiftData: คู่มือทีละขั้นตอน 2026
คู่มือฉบับสมบูรณ์สำหรับการย้ายแอป iOS จาก Core Data ไปยัง SwiftData พร้อมตัวอย่างใช้งานจริง กลยุทธ์การอยู่ร่วมกัน และแนวทางปฏิบัติที่ดี

Combine Framework: การเขียนโปรแกรมเชิงรีแอกทีฟใน Swift
เชี่ยวชาญ Combine สำหรับการจัดการสตรีมข้อมูลแบบ asynchronous ใน Swift: Publishers, Subscribers, Operators และรูปแบบขั้นสูงสำหรับแอป iOS

คำถามสัมภาษณ์การเข้าถึง iOS ในปี 2026: VoiceOver และ Dynamic Type
เตรียมตัวสัมภาษณ์ iOS ด้วยคำถามสำคัญเรื่องการเข้าถึง: VoiceOver, Dynamic Type, traits เชิงความหมาย และการตรวจสอบ.