Future và Subject trong Combine.
Bài đăng này đã không được cập nhật trong 4 năm
-
Mặc dù
Combineđã tập trung vàoconceptcácpublishersẽemitra cácsequence valuetheo dòng thời gian cũng như đã cung cấp một sốAPIthuận tiện và đầy đủ chức năng để người sử dụng không cần phải thiết lập tùy chỉnh cho cácpublishertừ đầu. -
Lấy ví dụ như khi chúng ta muốn
Combinehỗ trợ chúng ta với cácAPIcó sẵn nhưImageProcessorđể xử lý theocompletion handle patterntrongclosuretrong các hoạt độngasynchronouslykhi tiến trình xử lýimagehoàn tất hoặc thất bại:
struct ImageProcessor {
func process(
_ image: UIImage,
then handler: @escaping (Result<UIImage, Error>) -> Void
) {
// Process the image and call the handler when done
...
}
}
- Thay vì việc viết lại
ImageProcessorchúng ta có thể xử lý theo cách thức mới được giới thiệu trongCombine. Chúng ta không chỉ giữ được cáchimplementtrên mà vẫn có thể sử dụngCombineđể xử lýcompletion handletừng trường hợp ngay cả khi chúng ta thêmcodemới:
1/ Future được giới thiệu:
-
Chúng ta sẽ tập trung sử dụng
Futuretypeđược biết đến trongFuture/Promise pattern, 1patternrất phổ biến trong việc lập trình.Combinecung cấp cho chúng taclosurepromiseđể chúng ta có thể nhận biết khi cácoperationasynchronoushoàn tất cũng như sẽ tự độngmapResultvào các eventPublisher. -
Điều thực sự tiện lợi ở đây là trong trường hợp cụ thể bên trên với
completion handle closurecũ sử dụngResultlàInput, điều đó có nghĩa chúng ta có thể dụngFuturevới thiết lập đơn giản trongfuncprocesscũ như sau:
extension ImageProcessor {
func process(_ image: UIImage) -> Future<UIImage, Error> {
Future { promise in
process(image, then: promise)
}
}
}
- Chúng ta chỉ đơn giản là sử dụng một
completion handle closurechuyên dụng và tự chuyển kết quả thủ công vàopromise:
extension ImageProcessor {
func process(_ image: UIImage) -> Future<UIImage, Error> {
Future { promise in
process(image) { result in
promise(result)
}
}
}
}
- Với
Futurechúng ta cần có một cách triển khai chặt chẽ cácclosure APIcơ bản theo cáchreactivetrongCombinevàfuturecũng chỉ đơn giản làpublisher, tương đương với việc chúng ta có thể sử dụng cách sau:
processor.process(image)
.replaceError(with: .errorIcon)
.map { $0.withRenderingMode(.alwaysTemplate) }
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: imageView)
.store(in: &cancellables)
- Tuy nhiên thì
FuturetrongCombinechỉ có thểemitmột giá trịresultduy nhất và sẽ lập tức hoàn thành và được giải phóng khipromiseđược gọi đến.
2/ Xử lý nhiều loại giá trị Output:
- Quay lại với
closureImageProcessor, nếu chúng ta sử dụng 2closureở đây, 1 cái theo dõi cácupdatetrongprogressnhưimageđang đượcprocessvà 1 cái được gọi khi quá trình này kết thúc:
struct ImageProcessor {
typealias CompletionRatio = Double
typealias ProgressHandler = (CompletionRatio) -> Void
typealias CompletionHandler = (Result<UIImage, Error>) -> Void
func process(
_ image: UIImage,
onProgress: @escaping ProgressHandler,
onComplete: @escaping CompletionHandler
) {
// Process the image and call the progress handler to
// report the operation's ongoing progress, and then
// call the completion handler once the image has finished
// processing, or if an error was encountered.
...
}
}
- Trước hết chúng ta cần thêm vào
ProgressEventenumđể chỉ địnhtypechoOutputkhiPublisherđược chúng ta khởi tạo:
extension ImageProcessor {
enum ProgressEvent {
case updated(completionRatio: CompletionRatio)
case completed(UIImage)
}
}
- Mong muốn ban đầu là làm sao để
updatecácCombine APIbằng việc sử dụngFuturenhưng nay chúng ta sẽ sử dụngpromiseclosurenhiều lần để thông báo cácupdatetrong cáccompleted events:
extension ImageProcessor {
func process(_ image: UIImage) -> Future<ProgressEvent, Error> {
Future { promise in
process(image,
onProgress: { ratio in
promise(.success(
.updated(completionRatio: ratio)
))
},
onComplete: { result in
promise(result.map(ProgressEvent.completed))
}
)
}
}
}
- Tuy nhiên cách triển khai trên vẫn chưa hoạt động đúng như mong muốn vì chúng ta chỉ nhận được mỗi kết quả đầu tiên được
updatetrước khi tiến trình trên hoàn thành.
3/ Sử dụng Subject để gửi value:
-
Chúng ta có thể gửi nhiều
valuenhư chúng ta đã làm bên trên bằng cách sử dụng 2subjectchính được giới thiệu trongCombinePassthroughSubjecthoặcCurrentValueSubject. Chúng ta sẽ sử dụngPassthroughSubjectđể có thể truyềnvaluecho từngsubscribermà không giữ lạivaluenào. -
Chúng ta có thể sử dụng
subjectđểupdateImageProcessingcó thể hoạt động tốt cho cả việc theo dõiprogresscũng nhưcompleted event.
extension ImageProcessor {
func process(_ image: UIImage) -> AnyPublisher<ProgressEvent, Error> {
// First, we create our subject:
let subject = PassthroughSubject<ProgressEvent, Error>()
// Then, we call our closure-based API, and whenever it
// sends us a new event, then we'll pass that along to
// our subject. Finally, when our operation was finished,
// then we'll send a competion event to our subject:
process(image,
onProgress: { ratio in
subject.send(.updated(completionRatio: ratio))
},
onComplete: { result in
switch result {
case .success(let image):
subject.send(.completed(image))
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
}
}
)
// To avoid returning a mutable object, we convert our
// subject into a type-erased publisher before returning it:
return subject.eraseToAnyPublisher()
}
}
- Công việc còn lại của chúng ta phải
updateviệc sử dụngfunctrên trước khiProgressEventđược xử lý thay vì dùnginstanceUIImagenhư sau:
processor.process(image)
.replaceError(with: .completed(.errorIcon))
.receive(on: DispatchQueue.main)
.sink { event in
switch event {
case .updated(let completionRatio):
progressView.completionRatio = completionRatio
case .completed(let image):
imageView.image = image.withRenderingMode(
.alwaysTemplate
)
}
}
.store(in: &cancellables)
-
Hãy lưu ý khi sử dụng
PassthroughSubjectlà mỗisubscribersẽ chỉ nhậnvaluekhisubscriptionđã đượcactive -
Việc chuyển đổi giữa các
subjectthực sự rất đơn giản khi chúng ta dùngCurrentValueSubjectvới cácvaluehiện tại mà chúng ta cần theo dõi:
extension ImageProcessor {
func process(_ image: UIImage) -> AnyPublisher<ProgressEvent, Error> {
let subject = CurrentValueSubject<ProgressEvent, Error>(
.updated(completionRatio: 0)
)
...
return subject.eraseToAnyPublisher()
}
}
All rights reserved