-
Mặc dù
Combine
đã tập trung vàoconcept
cácpublisher
sẽemit
ra cácsequence value
theo dòng thời gian cũng như đã cung cấp một sốAPI
thuậ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ácpublisher
từ đầu. -
Lấy ví dụ như khi chúng ta muốn
Combine
hỗ trợ chúng ta với cácAPI
có sẵn nhưImageProcessor
để xử lý theocompletion handle pattern
trongclosure
trong các hoạt độngasynchronously
khi tiến trình xử lýimage
hoà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
ImageProcessor
chú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áchimplement
trên mà vẫn có thể sử dụngCombine
để xử lýcompletion handle
từng trường hợp ngay cả khi chúng ta thêmcode
mới:
1/ Future được giới thiệu:
-
Chúng ta sẽ tập trung sử dụng
Future
type
được biết đến trongFuture/Promise pattern
, 1pattern
rất phổ biến trong việc lập trình.Combine
cung cấp cho chúng taclosure
promise
để chúng ta có thể nhận biết khi cácoperation
asynchronous
hoàn tất cũng như sẽ tự độngmap
Result
và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 closure
cũ sử dụngResult
làInput
, điều đó có nghĩa chúng ta có thể dụngFuture
với thiết lập đơn giản trongfunc
process
cũ 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 closure
chuyê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
Future
chúng ta cần có một cách triển khai chặt chẽ cácclosure API
cơ bản theo cáchreactive
trongCombine
vàfuture
cũ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ì
Future
trongCombine
chỉ có thểemit
một giá trịresult
duy 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
closure
ImageProcessor
, nếu chúng ta sử dụng 2closure
ở đây, 1 cái theo dõi cácupdate
trongprogress
nhưimage
đang đượcprocess
và 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
ProgressEvent
enum
để chỉ địnhtype
choOutput
khiPublisher
đượ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 để
update
cácCombine API
bằng việc sử dụngFuture
nhưng nay chúng ta sẽ sử dụngpromise
closure
nhiều lần để thông báo cácupdate
trong 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
update
trướ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
value
như chúng ta đã làm bên trên bằng cách sử dụng 2subject
chính được giới thiệu trongCombine
PassthroughSubject
hoặcCurrentValueSubject
. Chúng ta sẽ sử dụngPassthroughSubject
để có thể truyềnvalue
cho từngsubscriber
mà không giữ lạivalue
nào. -
Chúng ta có thể sử dụng
subject
đểupdate
ImageProcessing
có thể hoạt động tốt cho cả việc theo dõiprogress
cũ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
update
việc sử dụngfunc
trên trước khiProgressEvent
được xử lý thay vì dùnginstance
UIImage
như 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
PassthroughSubject
là mỗisubscriber
sẽ chỉ nhậnvalue
khisubscription
đã đượcactive
-
Việc chuyển đổi giữa các
subject
thực sự rất đơn giản khi chúng ta dùngCurrentValueSubject
với cácvalue
hiệ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()
}
}