Async với PromiseKit trong ứng dụng iOS

1. Giới thiệu:

1.1. Synchronous & Asynchronous:

Đã là lập trình viên chắc các bạn đều biết về "đồng bộ" (Synchronous) và "bất đồng bộ" (Asynchronous).

Dưới đây là phần định nghĩa của 2 thuật ngữ này trên trang freetuts.net

Synchronous có nghĩa là xử lý đồng bộ, chương trình sẽ chạy theo từng bước và chỉ khi nào bước 1 thực hiện xong thì mới nhảy sang bước 2, khi nào chương trình này chạy xong mới nhảy qua chương trình khác. Đây là nguyên tắc cơ bản trong lập trình mà bạn đã được học đó là khi biên dịch các đoạn mã thì trình biên dịch sẽ biên dịch theo thứ tự từ trên xuống dưới, từ trái qua phải và chỉ khi nào biên dịch xong dòng thứ nhât mới nhảy sang dòng thứ hai, điều này sẽ sinh ra một trạng thái ta hay gọi là trạng thái chờ. Ví dụ trong quy trình sản xuất dây chuyền công nghiệp được coi là một hệ thống xử lý đồng bộ.

Ngược lại với Synchronous thì Asynchronous là xử lý bất động bộ, nghĩa là chương trình có thể nhảy đi bỏ qua một bước nào đó, vì vậy Asynchronous được ví như một chương trình hoạt động không chặt chẽ và không có quy trình nên việc quản lý rất khó khăn. Nếu một hàm A phải bắt buộc chạy trước hàm B thì với Asynchronous sẽ không thể đảm bảo nguyên tắc này luôn đúng.

1.2. Asynchronous trong iOS:

Trong iOS chúng ta có thể sử dụng Grand Central Dispatch (GCD) để thực hiện các hàm bất đồng bộ.

Với GCD chúng ta có thể tạo một hàm bất đồng bộ bằng cách tạo một task hay 1 code block và đưa vào queue của hệ thống. Task này sẽ được thực hiện trên thread khác và sẽ không làm block thread hiện tại:

DispatchQueue.global().async {
    // do something
}

Sau khi task thực hiện xong ta có thể cập nhật kết quả vào main thread:

DispatchQueue.global().async {
    // do something
    
    DispatchQueue.main.async {
        // update UI
    }
}

Để trả về kết quả của hàm bất đồng bộ ta có thể dùng block như sau:

func doSomeTask(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        // do something
        completion()
    }
}

Hoặc dùng delegate:

protocol ServiceDelegate: class {
    func serviceDidDoSomeTask()
}

class Service {
    weak var delegate: ServiceDelegate?
    func doSomeTask() {
        DispatchQueue.global().async { [weak self] in
            // do something
            self?.delegate?.serviceDidDoSomeTask()
        }
    }
}

Hoặc dùng Notification:

func doSomeTask() {
    DispatchQueue.global().async {
        // do something
        NotificationCenter.default.post(name: NSNotification.Name("FinishTaskNotification"), object: nil)
    }
}

2. PromiseKit:

PromiseKit là một thư viện mạnh mẽ giúp cho việc thực hiện các hàm async trở nên dễ dàng và thú vị hơn:

Ví dụ như dưới đây ta có 1 hàm login nếu viết theo cách thông thường:

func login(completion: @escaping (_ token: String) -> Void, failure: (_ error: Error) -> Void) {
    // send login request and return token or error
}

// using
login(completion: { (token) in
    
}) { (error) in
    print(error) 
}

Với Promise ta có thể viết đơn giản như sau:

func login() -> Promise<String> {
    // send login request and return token or error
}

login().then { token -> Void in
    // use token
}.catch { error in
    print(error)    
}

Trong block then chúng ta có thể return một đối tượng Promise để tạo thành chuỗi mắt xích (chain):

login().then { json -> Promise<UIImage> in
    return fetchAvatar(json["username"])
}.then { avatarImage in
    self.imageView.image = avatarImage
}

Khi ở một mắt xích bất kỳ có error thì error sẽ được xử lý ở block catch:

login().then {
    return fetchAvatar()
}.then { avatarImage in
    //…
}.catch { error in
    UIAlertView(/*…*/).show()
}

Thực tế chúng ta rất hay phải giải quyết bài toán phải chờ 1 vài tác vụ bất đồng bộ thực hiện xong rồi sau đó thực hiện 1 tác vụ khác, ví dụ như phải request 1 vài API sau đó thực hiện update UI. Việc này có thể thực hiện đơn giản trong Promise như sau:

func getProductList() -> Promise<[Product]> {}
func getReviewList() -> Promise<[Review]> {}

when(getProductList(), getReviewList()).then { products, reviews in
    // update UI
}.catch { error in
    print(error)
}

Nếu 1 trong 2 tác vụ trả về error thì when sẽ bị dừng ngay lập tức. Nếu bạn muốn chờ các tác vụ thực hiện xong bất chấp có trả về error hay không thì có thể dùng join:

join(getProductList(), getReviewList()).then { products, reviews in
    // update UI
}.catch { error in
    print(error)
}

Một bài toán khác là khi bạn có nhiều tác vụ cùng thực hiện và chỉ quan tâm tới kết quả trả về đầu tiên, ta có thể dùng race:

race(promise1, promise2, promise3).then { winner in
    //…
}

Lưu ý là race sẽ dừng lại bất cứ khi nào có error trả về.

Vậy, chúng ta sẽ tạo một đối tượng Promise như thế nào:

func doSomething() -> Promise<Void> {
    return Promise { fulfill, reject in
        DispatchQueue.global().async {
            // do something
           if success {
              fulfill()
           }
           else { // there is a error
              reject(error)
           }
        }
    }
}

Ví dụ với 1 framework nổi tiếng là Alamofire:

func request(url: String, requestType: HTTPMethod, parameters: [String: Any]?, encoding: ParameterEncoding, headers: [String: Any]?) -> Promise<Any?> {
    return Promise { fulfill, reject in
        Alamofire.request(url,
                          method: requestType,
                          parameters: parameters,
                          encoding: encoding,
                          headers: headers)
            .validate(statusCode: 200..<300)
            .responseJSON { (response) in
                switch response.result {
                case .success(let value):
                    fulfill(value)
                case .failure(let error):
                    reject(error)
                }
        }
    }
}

Ngoài ra thì Promise cũng cung cấp Extension cho rất nhiều Apple API cũng như các thư viện về network ví dụ như:

  • MapKit
  • CoreLocation
  • Alamofire
  • OMGHTTPURLRQ

3. Kết luận:

Nếu các bạn cảm thấy nhàm chán với việc phải viết delegate và block (đặc biệt khó chịu với từ khóa @escaping của Swift 3), hãy thử PromiseKit và tôi tin rằng bạn sẽ thấy việc viết code trở nên thú vị hơn nhiều.

Happy coding!