Combine vs async/await ใน Swift: รูปแบบการย้ายระบบแบบค่อยเป็นค่อยไป

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

การย้ายจาก Combine ไปยัง async/await ใน Swift พร้อมรูปแบบการอยู่ร่วมกัน

การมาถึงของ Swift Concurrency พร้อมกับ async/await ได้เปลี่ยนแปลงแนวปฏิบัติการเขียนโปรแกรมแบบไม่พร้อมกันบน iOS สำหรับโครงการที่ใช้ Combine คำถามเรื่องการย้ายระบบเกิดขึ้นโดยธรรมชาติ จำเป็นต้องเขียนใหม่ทั้งหมดหรือไม่ สองแนวทางสามารถอยู่ร่วมกันได้หรือไม่ รูปแบบใดที่ช่วยให้การเปลี่ยนผ่านราบรื่น คู่มือนี้สำรวจกลยุทธ์การย้ายระบบแบบค่อยเป็นค่อยไป ที่ช่วยให้นำ async/await มาใช้ได้โดยไม่ละทิ้ง Combine อย่างกะทันหัน

สิ่งที่คู่มือนี้ครอบคลุม

คู่มือนี้นำเสนอรูปแบบที่เป็นรูปธรรมสำหรับการย้ายระบบแบบค่อยเป็นค่อยไปจาก Combine ไปยัง async/await พร้อมตัวอย่างการเชื่อมโยงสองทิศทางและกลยุทธ์การอยู่ร่วมกันที่เหมาะสมกับโค้ดเบสที่มีอยู่

ทำความเข้าใจความแตกต่างพื้นฐาน

ก่อนเริ่มการย้ายระบบ การทำความเข้าใจสิ่งที่แยก Combine ออกจาก async/await เป็นเรื่องสำคัญ สองแนวทางนี้ตอบสนองความต้องการที่แตกต่างกัน และกรณีการใช้งานบางอย่างยังคงเหมาะกับ Combine มากกว่า

แบบจำลองทางความคิดของ Combine

Combine อิงจากแบบจำลองสตรีมข้อมูล Publisher ปล่อยค่าออกมาตามเวลา ตัวดำเนินการแปลงค่าเหล่านั้น และ Subscriber รับผลลัพธ์สุดท้าย แบบจำลองนี้โดดเด่นสำหรับสตรีมต่อเนื่อง เช่น เหตุการณ์ UI การแจ้งเตือน หรือ WebSocket

CombineExample.swiftswift
// 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 ใช้แบบจำลองตามลำดับ: การดำเนินการเริ่มต้น โค้ดรอผลลัพธ์ จากนั้นดำเนินการต่อ แบบจำลองนี้เป็นธรรมชาติมากกว่าสำหรับการดำเนินการครั้งเดียว เช่น คำขอเครือข่ายที่แยกออก หรือการอ่านไฟล์

AsyncAwaitExample.swiftswift
// 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

BridgingCombineToAsync.swiftswift
// 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 แบบกำหนดเองช่วยลดความซับซ้อนของการเชื่อมโยง

SingleValueBridging.swiftswift
// 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

BridgingAsyncToCombine.swiftswift
// 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 ที่สมบูรณ์ได้

AsyncSequencePublisher.swiftswift
// 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 อยู่ร่วมกันอย่างกลมกลืน

สถาปัตยกรรมแบบเป็นชั้นพร้อมการแยกนามธรรม

การกำหนดโปรโตคอลที่แยกการนำไปใช้ออกจากกัน ช่วยให้สามารถย้ายแบบค่อยเป็นค่อยไปโดยไม่ต้องแก้ไขโค้ดที่เรียกใช้

RepositoryAbstraction.swiftswift
// 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 สามารถเปิดเผยทั้งสองอินเทอร์เฟซในช่วงเปลี่ยนผ่าน

HybridViewModel.swiftswift
// 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

DebounceAsync.swiftswift
// 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

MergeAsync.swiftswift
// 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) เปล่งประกาย

UIEventsCombine.swiftswift
// 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

WebSocketCombine.swiftswift
// 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 สำหรับสตรีมเหตุการณ์ที่ต่อเนื่อง

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#swift
#ios
#combine
#async-await
#migration

แชร์

บทความที่เกี่ยวข้อง