Extending Combine in Swift.
Bài đăng này đã không được cập nhật trong 4 năm
- 
Combinecho phép chúng ta có thể thiết lập các quá trình xử lý bất đồng bộ như các luồng xử lý riêng biệt mà trong đó từng luồng lại có cácoperationkhác nhau. Các luồng xử lý riêng biệt có thể làobserved,transforned,conbinedbằng nhiều cách khác nhau và tất cả đều có thể được tiến hành xử lý với độ an toàn cao và được kiểm tra trongcompile time.
- 
Việc xác thực strong typerất hữu dụng khi chúng ta tiến hành mở rộng cácfunctrongCombinevì chúng ta có thể tạo các thay đổi tùy thuộc vàoout putchúng ta cần.
1 / Inserting default arguments:
- 
Mặc dù Combineđã cung cấp cho chúng ta rất nhiềuoperation transformnhưng đôi khi chúng ta vẫn cần tùy chỉnhAPIđể có thể sử dụng thuận tiện nhất trong quá trình phát triển. Một ví dụ điển hình là khi chúng chúng ta làm việc nhiều vớiprotocolPublishercótypeOutputvàFailurethì chúng ta cần biết chính xác loạitypemàpublisheryêu cầu:
- 
Operation decodeđược sử dụng đểtransformgiá trịDatađượcemitkhipublishersử dụngDecodeable. Tuy nhiênoperationnày yêu cầu chúng ta phải luôn truyền vàotypemà chúng ta cầndecode. Chúng ta cần tạo một tùy chỉnh nhỏ ở đây để có thể tự động thêm vào cho chúng ta các giá trị cần thiết cho công việc decode như:
extension Publisher where Output == Data {
    func decode<T: Decodable>(
        as type: T.Type = T.self,
        using decoder: JSONDecoder = .init()
    ) -> Publishers.Decode<Self, T, JSONDecoder> {
        decode(type: type, decoder: decoder)
    }
}
- Một điểm cần lưu ý ở thay đổi .init()vớiinstanceJSONDecoderđược sử dụng xuyên suốt app. Ví dụ như khi làm việc với webAPImà chúng ta download json từ cácsnake_case, chúng ta muốn thay thay thế cácsnakeCaseConvertinginstancenhư cácdecoderarguments:
extension JSONDecoder {
    static let snakeCaseConverting: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()
}
extension Publisher where Output == Data {
    func decode<T: Decodable>(
        as type: T.Type = T.self,
        using decoder: JSONDecoder = .snakeCaseConverting
    ) -> Publishers.Decode<Self, T, JSONDecoder> {
        decode(type: type, decoder: decoder)
    }
}
- Với thay đổi trên ở 2 extensiongiờ chúng ta có thể sử dụng.decode()đểdecodeJSON mỗi khicompilercho phép gợi ýoutput typemong muốn:
class ConfigModelController: ObservableObject {
    @Published private(set) var config: Config
    private let urlSession: URLSession
    
    ...
    func update() {
        // Here the compiler is able to infer which type that we’re
        // looking to decode our JSON into, based on the property
        // that we’re assigning our pipeline’s output to:
        urlSession.dataTaskPublisher(for: .config)
            .map(\.data)
            .decode()
            .replaceError(with: .default)
            .receive(on: DispatchQueue.main)
            .assign(to: &$config)
    }
}
- Operation- decodecủa chúng ta thực sự hữu dụng khi chúng ta đã có thể tùy chỉnh hoàn toàn và đưa ra- typemà chúng ta mong muốn cho- output:
cancellable = URLSession.shared
    .dataTaskPublisher(for: .allItems)
    .map(\.data)
    .decode(as: NetworkResponse.self)
    .sink { completion in
        // Handle completion
        ...
    } receiveValue: { response in
        // Handle response
        ...
    }
2/ Data validation:
- Tiếp theo chúng ta sẽ tìm hiểu xem chúng ta có thể làm gì để mở rộng các APIchuyên dụng cho công việc tiến hànhdata validation. Nếu chúng ta muốn mỗiNetworkResponseinstanceđã load đều chứa ít nhất 1Item. Cho ta có thể thêm vào cácAPIbằng cách sử dụngtryMapoperatorđể xử lýerrornếu có:
extension Publisher {
    func validate(
        using validator: @escaping (Output) throws -> Void
    ) -> Publishers.TryMap<Self, Output> {
        tryMap { output in
            try validator(output)
            return output
        }
    }
}
- Chúng ta cũng có thể thêm vào đoạn code trên một operatorvalidatecho các luồng làm việc trongCombinetrước đó và có thể xử lý phân loạierrorcho trường hợpitemsarray rỗng như sau:
cancellable = URLSession.shared
    .dataTaskPublisher(for: .allItems)
    .map(\.data)
    .decode(as: NetworkResponse.self)
    .validate { response in
    guard !response.items.isEmpty else {
        throw NetworkResponse.Error.missingItems
    }
}
    .sink { completion in
        // Handle completion
        ...
    } receiveValue: { response in
        // Handle response
        ...
    }
- Một phương thức khác để validate datatrongswiftlàunwrapping optionalsthông quaCombineoperatorcompactMapđể có thể bỏ qua các giá trịnilthay vì ném raerrorkhi mà công việc xử lý không cần trả ra kết quả. Nhưng nếu chung ta muốn show ra cácerrortrong các trường hợp chúng ta có thể custom nó như sau:
extension Publisher {
    func unwrap<T>(
        orThrow error: @escaping @autoclosure () -> Failure
    ) -> Publishers.TryMap<Self, T> where Output == Optional<T> {
        tryMap { output in
            switch output {
            case .some(let value):
                return value
            case nil:
                throw error()
            }
        }
    }
}
- Công dụng của unwrapoperator là khi chúng ta đang load một collectionItemkhác từrecentItemsvà chúng ta chỉ cầnelementđầu tiên thì chúng ta làm như sau:
cancellable = URLSession.shared
    .dataTaskPublisher(for: .recentItems)
    .map(\.data)
    .decode(as: NetworkResponse.self)
    .map(\.items.first)
.unwrap(orThrow: NetworkResponse.Error.missingItems)
    .sink { completion in
        // Handle completion
        ...
    } receiveValue: { response in
        // Handle response
        ...
    }
3/ Result conversions:
- 
Cách phổ biến nhất để xử lý outputtừCombinelà sử dụngsinkoperator với 2closure, 1 dùng để xử lý từngoutput valuevà cái còn lại đểcompletion event.
- 
Tuy nhiên khi xử lý thành công outputvà xử lýcompletionriêng biệt đôi khi lại đem lại cho chúng ta bất tiện nhất định. Trong trường hợp này việcemitriêng mộtResultvalue lại là một cách tuyệt vời để chúng ta tiến hành tùy chỉnh cácoperator:
extension Publisher {
    func convertToResult() -> AnyPublisher<Result<Output, Failure>, Never> {
        self.map(Result.success)
            .catch { Just(.failure($0)) }
            .eraseToAnyPublisher()
    }
}
- Operatior mới convertToResultphát huy được thế mạng khi sử dụngbuildviewmodelchoSwiftUIview. Chúng ta bây giờ sẽ duyên từ cácitemđã load dược trước đó trongItemListViewModelđể sử dụng chooperatormới để có thểswitchsangResultcho mỗioperationđang loading:
class ItemListViewModel: ObservableObject {
    @Published private(set) var items = [Item]()
    @Published private(set) var error: Error?
    private let urlSession: URLSession
    private var cancellable: AnyCancellable?
    ...
    func load() {
        cancellable = urlSession
            .dataTaskPublisher(for: .allItems)
            .map(\.data)
            .decode(as: NetworkResponse.self)
            .validate { response in
                guard !response.items.isEmpty else {
                    throw NetworkResponse.Error.missingItems
                }
            }
            .map(\.items)
            .convertToResult()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .success(let items):
                    self?.items = items
                    self?.error = nil
                case .failure(let error):
                    self?.items = []
                    self?.error = error
                }
            }
    }
}
- Tiếp tục tìm hiểu sâu hơn chúng ta sẽ update lại view modelbằng cách dùngLoadableObjectprotocol vàLoadingStateenum. Chúng ta bắt đầu add thêm các operator tùy chỉnh đểemitLoadingStatevalue thay vì dùngResult:
extension Publisher {
    func convertToLoadingState() -> AnyPublisher<LoadingState<Output>, Never> {
        self.map(LoadingState.loaded)
            .catch { Just(.failed($0)) }
            .eraseToAnyPublisher()
    }
}
- Thay đổi trên giúp chúng ta dễ dàng load và assign state cho viewmodel như sau:
class ItemListViewModel: LoadableObject {
    @Published private(set) var state = LoadingState<[Item]>.idle
    
    ...
    
    func load() {
    	guard !state.isLoading else {
            return
    	}
    
    	state = .loading
    
        urlSession
            .dataTaskPublisher(for: .allItems)
            .map(\.data)
            .decode(as: NetworkResponse.self)
            .validate { response in
                guard !response.items.isEmpty else {
                    throw NetworkResponse.Error.missingItems
                }
            }
            .map(\.items)
            .receive(on: DispatchQueue.main)
            .convertToLoadingState()
.assign(to: &$state)
    }
}
4/ Type-erased constant publishers:
- Thỉnh thoảng chúng ta muốn tạo một publisherchỉemitduy nhất mộtvaluehoặcerrorbằng cách sử dụngJusthoặcFail. Tuy nhiên khi sử dụng cácpublishernày chúng cũng phải tiến hành các operator kèm theo nhưsetFailureTypevàeraseToAnyPublisher:
class SearchResultsLoader {
    private let urlSession: URLSession
    private let cache: Cache<String, [Item]>
    
    ...
    func searchForItems(
        matching query: String
    ) -> AnyPublisher<[Item], SearchError> {
        guard !query.isEmpty else {
            return Fail(error: SearchError.emptyQuery)
    .eraseToAnyPublisher()
        }
        if let cachedItems = cache.value(forKey: query) {
            return Just(cachedItems)
    .setFailureType(to: SearchError.self)
    .eraseToAnyPublisher()
        }
        return urlSession
            .dataTaskPublisher(for: .search(for: query))
            .map(\.data)
            .decode(as: NetworkResponse.self)
            .mapError(SearchError.requestFailed)
            .map(\.items)
            .handleEvents(receiveOutput: { [cache] items in
                cache.insert(items, forKey: query)
            })
            .eraseToAnyPublisher()
    }
}
- Tiếp đến chúng ta hãy thử mở rộng AnyPublishervới 2staticmethod, 1 cho việc tạotype-erasedJustvà một choFailpublisher:
extension AnyPublisher {
    static func just(_ output: Output) -> Self {
        Just(output)
            .setFailureType(to: Failure.self)
            .eraseToAnyPublisher()
    }
    
    static func fail(with error: Failure) -> Self {
        Fail(error: error).eraseToAnyPublisher()
    }
}
- Bây giờ chúng ta đã có thể sử dụng Swift"dot syntax" để tạoconstantpublisher với một dòng code như sau:
class SearchResultsLoader {
    ...
    func searchForItems(
        matching query: String
    ) -> AnyPublisher<[Item], SearchError> {
        guard !query.isEmpty else {
            return .fail(with: SearchError.emptyQuery)
        }
        if let cachedItems = cache.value(forKey: query) {
            return .just(cachedItems)
        }
        ...
    }
}
All rights reserved
 
  
 