Mục tiêu của bài viết này là vẽ bức tranh lớn về cách sử dụng khung Kết hợp trong Swift.

Combine là gì?

Combine là framework để khai báo và để xử lý các giá trị theo thời gian thực trong Swift. Nó áp dụng mô hình phản ứng(reactive) trong lập trình – Nếu có một sự kiện gì đó xảy ra thì chúng ta sẽ nhận được phản ứng để xử lý, khác với mô hình hướng đối tượng đang thịnh hành trong cộng đồng phát triển iOS. Các framework nổi tiếng chúng ta thường dùng có áp dụng reactive như: RxSwift, RxCoccoa, React Native,….

Reactive có nghĩa là lập trình với các luồng giá trị một cách không đồng bộ.

Lập trình hướng chức năng(Functional programming) là lập trình với các hàm. Trong các hàm của Swift có thể được truyền dưới dạng đối số cho các hàm khác, được trả về từ các hàm khác, được lưu trữ trong các biến hoặc struct và được xây dựng dưới dạng các closures.

Trong phong cách lập trình khai báo(declarative), bạn mô tả những gì chương trình làm, mà không mô tả các flow điều khiển chương trình. Trong phong cách tạo ra các mệnh lệnh, bạn viết cách chương trình hoạt động bằng cách thực hiện và xử lý một loạt các nhiệm vụ. Các chương trình theo phong cách mệnh lệnh sẽ chủ yếu dựa vào các trạng thái(state), theo dõi các trạng thái có được sửa đổi bởi một ai đó hoặc thứ gì đó không.

Lập trình với Swift Combine framework sẽ bao gồm có việc khai báo(declarative), phản ứng(reactive) và các hàm(functional). Nó liên quan đến các chuỗi hàm và truyền các giá trị từ cái này sang cái khác. Điều này tạo ra các luồng giá trị, chảy từ đầu vào đến đầu ra.

Bạn có thể hình dung Combine sẽ bao gồm sự kết hợp giữ các đối tượng như hình sau đây:

Có thể hiểu ngắn hơn như sau:

Combine = Publishers + Subscribers + Operators

OK, Bây giờ chúng ta sẽ tìm hiểu từng các phần tử ở trên để hiểu sâu hơn về Combile, về cách nó hoạt động.

Publisher là gì?

Publisher là một nhà phát hành hay xuất bản một cái gì đó(có thể phát hành ra giá trị hay sự kiện gì đó).

Publisher sẽ gửi các chuỗi giá trị theo thời gian cho một hoặc nhiều Subscribers(Người đăng ký).

Một Combine publishers cần phải kế thừa “protocol Publisher” sau:

protocol Publisher {
    associatedtype Output
    associatedtype Failure : Error
    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

Publisher có thể gửi các giá trị hoặc chấm dứt với thành công hoặc lỗi. Output để định nghĩa loại giá trị nào mà Publisher có thể gửi. Failure để định nghĩa kiểu lỗi khi nó gặp lỗi.

Hàm receive(subscriber:) là phương thức kết nối một subscriber với một publisher. Nó định nghĩa một hợp đồng giữa: đầu ra của publisher phải phù hợp với đầu vào của subscriber, và các kiểu lỗi cũng vậy.

Subscriber là gì?

Subscriber là người nhận giá trị gì đó. Nó sẽ nhận giá trị gì đó từ publisher.

Một Combine subscribers cần phải kế thừa “protocol Subscriber” dưới đây:

public protocol Subscriber : CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure : Error

    func receive(subscription: Subscription)
    func receive(_ input: Self.Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Self.Failure>)
}

Một subscriber có thể nhận được giá trị có Kiểu dữ liệu tạiInput hoặc sự kiện chấm dứt với thành công hoặc Failure.

Ba hàm receive ở trên là các phương pháp mô tả các bước xử lý khác nhau đối với vòng đời của subscriber. Chúng ta sẽ quay lại chủ đề này trong các đoạn tiếp theo.

Kết nối Publisher đến Subscriber

Combine có 2 loại subscribers: Subscribers.Sink và Subscribers.Assign . Bạn có thể kết nối chúng bằng cách gọi một trong hai phương thức này trên publisher:

  • sink(receiveCompletion:receiveValue:)để xử lý phần tử mới hoặc sự kiện hoàn thành trong một closure.
  • assign(to:on:) để viết phần tử mới vào một property nào đó.
let publisher = Just(1)

publisher.sink(receiveCompletion: { _ in
    print("finished")
}, receiveValue: { value in
    print(value)
})

Line 1: Tạo một publisher kiểuJust để nó gửi một giá trị và sau đó hoàn thành(completes). Combine có một số loại publisher, trong đó có Just.

Line 3: Kết nối tới một subscriber là Subscribers.Sink .

Nó sẽ in:

1
finished

Sau khi gửi 1, publisher sẽ tự động hoàn tất. Chúng ta không phải xử lý bất kỳ lỗi nào, vì publisher kiểuJust sẽ không có thất bại.

Subjects là gì?

Subject là một kiểu Publisher đặt biệt, nó có thể chèn các giá trị, được truyền từ bên ngoài vào luồng. Subject cung cấp cho chúng ta ba cách khác nhau để gửi các thành phần nào đó:

public protocol Subject : AnyObject, Publisher {
    func send(_ value: Self.Output)
    func send(completion: Subscribers.Completion<Self.Failure>)
    func send(subscription: Subscription)
}

Combine có 2 loại Subject: PassthroughSubjectCurrentValueSubject:

let subject = PassthroughSubject<String, Never>()

subject.sink(receiveCompletion: { _ in
    print("finished")
}, receiveValue: { value in
    print(value)
})

subject.send("Hello,")
subject.send("World!")
subject.send(completion: .finished)

Line 1: Tạo một PassthroughSubject. Chúng ta đặt kiểu Failure thành Never để chỉ ra rằng nó luôn kết thúc thành công.
Line 3: Subscribe một subject chúng ta mới tạo (hãy nhớ, đoạn này vẫn đang tạo một publisher).
Line 11: Gửi hai giá trị cho luồng(stream) và sau đó hoàn thành nó.

Nó sẽ in:

Hello,
World!
finished

CurrentValueSubject phải được tạo với một giá trị ban đầu và xuất bản tất cả những thay đổi của nó. Nó có thể trả về giá trị hiện tại của nó thông qua thuộc tính value.

let subject = CurrentValueSubject<Int, Never>(1)

print(subject.value)

subject.send(2)
print(subject.value)

subject.sink { value in
    print(value)
}

Line 1: Tạo một subject với giá trị ban đầu là 1.
Line 3: In giá trị hiện tại.
Line 5: Cập nhật giá trị hiện tại lên 2.
Line 6: Subscribe thèn publisher mới tạo ở trên nay nói đơn giản là theo dõi thèn subject ở trên.

Kq sẽ in:

1
2
2

Vòng đời của Combine Publisher and Subscriber

Một kết nối giữa một publisher và một subscriber được gọi là đăng ký(subscription). Các bước kết nối như vậy được gọi là vòng đời của publisher-subscriber.

let subject = PassthroughSubject<Int, Never>()

let token = subject
    .print()
    .sink(receiveValue: { print("received by subscriber: \($0)") })

subject.send(1)

Lưu ý toán tử print(_:to:) trong đoạn trích trên. Nó in các thông điệp cho tất cả các sự kiện của publisher tạo ra lên console, điều này cho chúng ta biết và hiểu về vòng đời của chúng.

Đây là những gì được in ra console:

receive subscription: (PassthroughSubject)
request unlimited
receive value: (1)
received by subscriber: 1
receive cancel

Điều này cho chúng ta thấy về vòng đời của publisher-subscriber. Với một số bước bị thiếu trong debug log, Chúng ta cùng kiểm tra khi nào thì vòng đời của nó kết thúc.

  1. Một Subscriber kết nối với publisher bằng cách gọi subscribe<S>(S).
  2. Một publisher tạo một đăng ký bằng cách gọi receive<S>(subscriber: S) trên chính nó.
  3. Publisher báo rằng nó nhận được yêu cầu đăng ký. Bằng cách gọi receive(subscription:) trên Subscriber.
  4. Subscriber yêu cầu một số thành phần mà nó muốn nhận. Nó gọi request (:) trên subscription và thông qua Demand (Nhu cầu) dưới dạng tham số. Demand là nơi định nghĩa một publisher có thể gửi bao nhiêu thành phần cho một subscriber thông qua đăng ký(subscription). Trong trường hợp của chúng ta, Demand là không giới hạn(unlimited).
  5. Publisher gửi các giá trị bằng cách gọi receive(_:) trên subscriber. Phương thức này sẽ trả về một Demand instance, cho biết có bao nhiêu thành phần mà subscriber mong muốn nhận được. Subscriber chỉ có thể tăng nhu cầu hoặc để nguyên như vậy, nhưng không thể giảm.
  6. Việc đăng ký kết thúc với một trong những kết quả sau:
  • Cancelled, Điều này có thể xảy ra tự động khi subscriber được huỷ, được dùng trong ví dụ trên. Một cách khác là hủy bằng tay: token.cancel().
  • Kết thúc thành công.
  • Xảy ra lỗi gì đó.

Token thực chất là subscriber, ban có thể tham khảo protocol AnyCancellable

Một chuỗi các Publishers với các toán tử

Toán tử(Operators) là các phương thức đặc biệt được gọi trên Publishers và trả về một Publishers khác. Điều này cho phép áp dụng một loại các Publishers khác nhau một cách lần lượt và tạo ra một chuỗi. Mỗi toán tử sẽ tạo ra một Publisher nào đó và nó sẽ trả về từ toán tử trước đó.

Một chuỗi phải được bắt nguồn bởi một publisher. Sau đó, các toán tử có thể được áp dụng lần lượt. Mỗi toán tử nhận được publisher mới được tạo bởi toán tử trước đó trong chuỗi. Chúng ta gọi thứ tự tương đối của chúng là UpstreamDownstream, nghĩa là toán tử ngay trước và tiếp theo.

Chúng ta xem cách xâu chuỗi các publisher khi xử lý yêu cầu URL HTTP với Combine Swift framework.

let url = URL(string: "https://api.github.com/users/V8tr/repos")!

let token = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: [Repository].self, decoder: JSONDecoder())
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { repositories in
        print("Cafedevn has \(repositories.count) repositories")
    })

Line 1: Tạo request để lấy các GitHub repositories. Chúng ta đang sử dụng GitHub REST API.

Line 3: Combine được tích hợp tốt vào các Framework Swift và SDK iOS. Điều này cho phép chúng ta sử dụng tích hợp publisher để xử lý các tác vụ dữ liệu của URLSession.

Line 4: Truyền dữ liệu nhận được từ url. Chúng ta sử dụng toán tử map (_ :), biến đổi giá trị đó.

Line 5: Decode nội dung từ response bằng JSONDecoder

Line 6: Kết nói với một subscriber là sink. Nó sẽ in ra số lượng repositories nhận được và hoàn thành.

Nó sẽ in:

Cafedevn has 30 repositories
finished

Các tài liệu các bạn nên tham khảo khi học Combine

Tôi khuyên bạn nên xem các video WWDC nói về Combine:

Ngoài ra, tôi khuyên bạn nên đọc đặc tả Reactive Streams, nó là tiêu chuẩn để xử lý luồng không đồng bộ. Nó cho phép hiểu rõ hơn các khái niệm Combine cốt lõi: nhà xuất bản, người đăng ký và đăng ký.(publishers, subscribers and subscriptions.)

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!

1 BÌNH LUẬN

Bình luận bị đóng.