Trong lập trình Swift thì việc viết & khai báo khối lệnh bất đồng bộ (asynchronous code) là khá phức tạp và dễ bị dẫn đến trường hợp callback hell. Tuy nhiên thì nhờ có thư viện PromiseKit thì việc này đã trở nên dễ dàng và tối ưu hơn nhiều.

Để dễ hình dung thì ở đây tôi có một ví dụ về callback hell, ở đây chúng ta có một tác vụ load ảnh GIF từ Giphy API:

giphy.fetchRandomGifUrl(forSearchQuery: "SNES") { imageUrlString, error in
  if error {
    print(error.localizedDescription)
    throw error
  }
  self.fetchImage(forImageUrl: imageUrlString) { imageData, error in
    if error {
      print(error.localizedDescription)
      throw error
    }
    self.attachImage(withImageData: imageData)
  }
}

Bạn có thế thấy các closure đã bị lồng ghép vào nhau và mỗi level lại có một luồng check error riêng biệt, cách viết này làm code trở nên rất phức tạp và khó đọc nếu như chúng ta xử lý các tác vụ khác phức tạp hơn nữa. Và đây là cách PromiseKit xử lý task trên:

giphy.fetchRandomGifUrl(forSearchQuery: "SNES").then { imageUrlString in
  self.fetchImage(forImageUrl: imageUrlString)
}.then { imageData in
  self.attachImage(withImageData: imageData)
}.catch { error in
  print(error.localizedDescription)
}

Ở đây chúng ta sử dụng .then() để thực thi tuần tự các request và dùng .catch() để hứng error, và không cần phải lặp đi lặp lại việc viết error handling code nữa. Bạn có thể thấy đoạn code trên clear và dễ đọc hiểu hơn rất nhiều.

Bài viết này chúng ta cùng viết một ứng dụng nhỏ có tính năng hiển thị ảnh gif các trò chơi của hệ máy SNES, chúng ta sẽ sử dụng Promises để điều khiển luồng networking. Đầu tiên chúng ta tải starter project về, để tiết kiệm thời gian thì ở đây đã thiết lập sẵn khung sườn cố định của project và chúng ta chỉ tập trung vào phần quan trọng nhất là networking, sau đó chúng ta mở Terminal lên và gõ đoạn mã lệnh sau:

git clone git@github.com:sagnew/promises-swift-tutorial.git
cd promises-swift-tutorial/SNESGifs
git checkout tutorial

Ứng dụng này có một cài thêm một vào thư viện sau:

  • Alamofire để thực hiện tác vụ gửi request tới Giphy API.
  • SwifyJSON xử lý dữ liệu JSON trả về từ API
  • SwiftyGif để hiển thị file Gif
  • PromiseKit cho cơ chế promises.

Để cài đặt các thư viện trên thì chúng ta sẽ sử dụng CocoaPods.

Đầu tiên tại GiphyManager.swift, chúng ta thêm function để lấy về URL từ Giphy API:

func fetchRandomGifUrl(forSearchQuery query: String) -> Promise<String> {
  let params = ["api_key": self.apiKey, "q": query, "limit": "\(self.imageLimit)"]
 
  // Return a Promise for the caller of this function to use.
  return Promise { fulfill, reject in
 
    // Inside the Promise, make an HTTP request to the Giphy API.
    Alamofire.request(self.giphyBaseURL, parameters: params)
      .responseJSON { response in
        if let result = response.result.value {
          let json = JSON(result)
          let randomNum = Int(arc4random_uniform(self.imageLimit))
 
          if let imageUrlString = json["data"][randomNum]["images"]["downsized"]["url"].string {
            fulfill(imageUrlString)
          } else {
            reject(response.error!)
          }
 
        }
    }
  }
}

Lưu ý rằng thay vì trả về một completion handler function thì chúng ta trả về một object Promise. Khi lấy được URL từ API thì chúng ta sẽ truyền nó qua function fulfill function. Nếu request thất bại, chúng ta gọi đến reject function.

Sau khi đã có URL thì chúng ta cần tải image đó và load vào trong UIIMageView. Trỏ đến  ViewController.swift và khai báo function để tải image từ URL:

func fetchImage(forImageUrl imageUrlString: String) -> Promise<Data> {
 
  // Return a Promise for the caller of this function to use.
  return Promise { fulfill, reject in
 
    // Make an HTTP request to download the image.
    Alamofire.request(imageUrlString).responseData { response in
 
      if let imageData = response.result.value {
        print("image downloaded")
 
        // Pass the image data to the next function.
        fulfill(imageData)
      } else {
 
        // Reject the Promise if something went wrong.
        reject(response.error!)
      }
    }
  }
}

Function này cũng tương tự với hàm fetchRandomGifUrl ở trên và xử lý theo hai trường hợp fullfills và rejects. Việc sử dụng Promises giúp chúng ta "đi tắt" được một quãng đường khá dài. Và cuối cùng chúng ta viết một function nhỏ để gắn hình ảnh vào trong UIIMageView bằng cách dùng SwiftyGif:

func attachImage(withImageData imageData: Data) -> Void {
  let image = UIImage(gifData: imageData)
  self.snesGifImageView.setGifImage(image)
}

Tới đây thì chúng ta đã đi qua hai tác vụ, và để tránh code bị rối rắm, hãy tận dụng tính năng .then() để lồng ghép các tác vụ trên vào làm một.

override func viewDidLoad() {
  super.viewDidLoad()
  let giphy = GiphyManager()
 
  giphy.fetchRandomGifUrl(forSearchQuery: "SNES").then { imageUrlString in
    self.fetchImage(forImageUrl: imageUrlString)
  }.then { imageData in
    self.attachImage(withImageData: imageData)
  }.catch { error in
    print(error)
  }
 
}

Như vậy là mọi tác vụ đã được gói gọn vào một chỗ, làm code của bạn trở nên sáng sủa và dễ hiểu hơn rất nhiều rồi đó. Build và run ứng dụng tận hưởng thành quả của mình thôi.