Alamofire - Elegant networking in swift. Part 1: Getting started

Như chúng ta đã biết thì AFNetworking là một trong những thư viện phổ biến nhất được viết cho iOSOSX. Năm 2012 nó đã nhận được danh hiệu 2012 Best iOS Library Award và nó là thư viện mã nguồn mở được sử dụng nhiều nhất trong các dự án (tại Github với hơn 14K stars và 4K folks)

Gần đây, cùng tác giả Mattt Thompson đã phát hành một thư viện networking mới giống như AFNetworking nhưng được thiết kế đặc biệt để phù hợp với ngôn ngữ và convention Swift. Nó có tên là Alamofire.

Alamofire chính là tên viết tắt của AF trong AFNetworking và thư viện mới này sử dụng convention của SWift. Tôi sẽ dành 2 part để viết về thư viện mã nguồn mở Alamofire trong series này.

  • Trong part 1 này tôi sẽ hướng dẫn các bạn sử dụng thư viện Alamofire để xây dựng ứng dụng Photo gallery với nguồn ảnh được lấy từ https://500px.com. Trong quá trình làm bạn sẽ hiểu được về các thành phần quan trọng của Alamofire và một vài thứ quan trọng khác nhằm quản lý các network request trong ứng dụng của bạn.

  • Part 2 sẽ tiếp tục part 1 và bạn sẽ hiểu thêm một vài chức năng khác và một số đặc trưng của Alamofire

Bắt đầu

Bạn có thể download start project theo đường link Photomania_Starter_6.4.zip để bắt đầu. Trong đó nó đã code phần giao diện, nhưng trong tutorial này bạn nên quan tâm các sử dụng Alamofire hơn là giao diện.

Bạn mở project bằng Xcode và nhìn vào file Main.storyboard

11-700x338.png

Trong ứng dụng này có sử dụng UITabBarController làm rootViewController. Tab view controller bao gồm 2 tab, mỗi tab là một UINavigationController. Tab thứ nhất cho phép người dùng duyệt các ảnh, tab thứ 2 duyệt các ảnh đã được người dùng lưu lại. Cả hai đều sử dụng UICollectionViewController để hiển thị ảnh. Và Main.storyboard chứa toàn bộ các view controller sẽ được dùng trong tutorial này.

Bạn build và run ứng dụng sẽ được như sau:

Spinner-179x320.png

Để có thể sử dụng Alamofire mới nhất bạn truy cập tại Alamofire và click vào button download Zip. Bạn mở project và kéo folder Alamofire-master vào trong project của bạn.

DragFolder-480x224.png

Bạn mở folder folder Alamofire-master và kéo Alamofire.xcodeproj (file có icon màu xanh)

0.png

Tiếp theo bạn click vào Photomania và đảm bảo bạn đã chọn General tab. Cuộn xuống và click vào + bên dưới Embedded Binaries, chọn Alamofire.framework và click Add.

0.1-637x500.png

Bạn buil và run project đảm bảo không lỗi để thực tiếp các bước tiếp theo.

Tiếp theo công việc là lấy dữ liệu về với Alamofire

  • Có thể bạn đang đặt câu hỏi vì sao Alamofire lại là thư viện được dùng để thay thế mà không phải là một thư viện khác? Apple cung cấp class NSURLSession và một số class khác liên quan, tại sao chúng ta vẫn cần tới thư viện Alamofire?

  • Câu trả lời là Alamofire cũng dựa trên nền tảng NSURLSession nhưng bạn hãy thử so sánh việc sử dụng NSURLSessionAlamofire sau tutorial này xem thế nào nhé. Việc bạn viết code có sử dụng Alamofire thì dễ viết code, rõ ràng, đơn giản. Bạn có thể truy cập internet với rất ít nỗ lực, code trong sáng, dễ đọc.

Để có thể sử dụng Alamofire thì đầu tiên bạn cần phải import nó.

import Alamofire

Việc import này được thực hiện tại class mà bạn sử dụng Alamofire

Tiếp theo bạn add đoạn code sau và viewDidLoad() và sau setupView()

Alamofire.request(.GET, "https://api.500px.com/v1/photos").responseJSON() {
  (_, _, data, _) in
  println(data)
}

Tôi sẽ giải thích luôn, nhưng bạn hãy build và run ứng dụng. Và bạn sẽ nhìn thấy message sau:

Optional({
    error = "Consumer key missing.";
    status = 401;
})

Tuy nó là lỗi nhưng bạn đã tạo được request đầu tiên với Alamofire và bạn đã nhận được response là file json trả về.

  • Alamofire.request(:) với hai tham số là method (POST, GET, ...) và URL (dạng string)
  • Thông thường chỉ đơn giản là một chuỗi các request object, ví dụ với đoạn code trên thì responseJSON() chỉ đơn giản cung cấp cho bạn một closure khi request được hoàn thành. Trong tutorial này thì response trả ra dạng JSON được parse trong console.
  • Bằng cách bạn call responseJSON thì bạn sẽ nhận được phản hồi là JSON như bạn mong muốn. Trong trường hợp này thì Alamofire sẽ cố gắng trả ra cho bạn đối tượng JSON. Cuối cùng bạn có thể request danh sách các thuộc tính bằng cách sử dụng responsePropertyList hoặc get string bằng cách sử dụng responseString. Bạn sẽ biết rõ hơn trong tutorial sau.

Tiếp theo, điều quan trọng chính là nguồn dữ liệu ở đâu?

Phía trên tôi đã đề cập chúng ta sẽ lấy ảnh từ nguồn https://500px.com. Nhưng chúng ta cần có key bằng cách register. Truy cập Signup và register với một email của bạn hoặc có thể từ facebook của bạn hay một số mạng xã hội khác. Khi hoàn thành bạn hãy vào Settings và click vào register your application. Bạn sẽ thấy như hình sau:

2-406x500.png

Sau khi register xong bạn sẽ nhận được chi tiết sau:

4.png

Bạn hãy copy consumer key và add vào parameters như sau:

Alamofire.request(.GET, "https://api.500px.com/v1/photos", parameters: ["consumer_key": "PASTE_YOUR_CONSUMER_KEY_HERE"]).responseJSON() {
  (_, _, JSON, _) in
  println(JSON)
}

Bạn cần đảm bảo PASTE_YOUR_CONSUMER_KEY_HERE được nhập vào là key của bạn đã copy bên trên. Bạn build và run ứng dụng sẽ thấy log từ console.

4.5-700x163.png

Và dưới đây là JSON:

{
  "feature": "popular",
  "filters": {
      "category": false,
      "exclude": false
  },
  "current_page": 1,
  "total_pages": 250,
  "total_items": 5000,
  "photos": [
    {
      "id": 4910421,
      "name": "Orange or lemon",
      "description": "",
	.
	.
	.
      }
    },
    {
      "id": 4905955,
      "name": "R E S I G N E D",
      "description": "From the past of Tagus River, we have History and memories, some of them abandoned and disclaimed in their margins ...",
	.
	.
	.
    }
  ]
}

Thay print(JSON) trong viewDidLoad() thành code sau:

let photoInfos = (JSON!.valueForKey("photos") as! [NSDictionary]).filter({
    ($0["nsfw"] as! Bool) == false
  }).map {
    PhotoInfo(id: $0["id"] as! Int, url: $0["image_url"] as! String)
  }
self.photos.addObjectsFromArray(photoInfos)
self.collectionView!.reloadData()

Bạn build và run ứng dụng được như sau:

4.6-307x500.png

Bước tiếp theo cần hiển thị dữ liệu lên giao diện

Mở PhotoBrowserCollectionViewController.swift và thêm đoạn code sau vào collectionView(_: cellForItemAtIndexPath:) trước khi return cell

let imageURL = (photos.objectAtIndex(indexPath.row) as! PhotoInfo).url
Alamofire.request(.GET, imageURL).response() {
  (_, _, data, _) in

  let image = UIImage(data: data!)
  cell.imageView.image = image
}

Bạn build và run ứng dụng:

5-307x500.jpg

Vậy câu hỏi đặt ra làm sao bạn có thể dễ dàng quản lý các API và khi viết code không cần copy/paste với những rủi ro không lường trước? Câu trả lời là bạn cần tạo ra một request router. Bạn mở Five100px.swiftimport Alamofire.

enum Router: URLRequestConvertible {
  static let baseURLString = "https://api.500px.com/v1"
  static let consumerKey = "PASTE_YOUR_CONSUMER_KEY_HERE"

    case PopularPhotos(Int)
    case PhotoInfo(Int, ImageSize)
    case Comments(Int, Int)

    var URLRequest: NSURLRequest {
      let (path: String, parameters: [String: AnyObject]) = {
        switch self {
        case .PopularPhotos (let page):
          let params = ["consumer_key": Router.consumerKey, "page": "\(page)", "feature": "popular", "rpp": "50",  "include_store": "store_download", "include_states": "votes"]
          return ("/photos", params)
        case .PhotoInfo(let photoID, let imageSize):
          var params = ["consumer_key": Router.consumerKey, "image_size": "\(imageSize.rawValue)"]
          return ("/photos/\(photoID)", params)
        case .Comments(let photoID, let commentsPage):
          var params = ["consumer_key": Router.consumerKey, "comments": "1", "comments_page": "\(commentsPage)"]
          return ("/photos/\(photoID)/comments", params)
        }
        }()

        let URL = NSURL(string: Router.baseURLString)
        let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path))
        let encoding = Alamofire.ParameterEncoding.URL

        return encoding.encode(URLRequest, parameters: parameters).0
  }
}

Tạm thời chúng ta cần phải có Loading More Photos Bạn mở PhotoBrowserCollectionViewController.swift và thêm đoạn code dưới vào sau let refreshControl = UIRefreshControl():

var populatingPhotos = false
var currentPage = 1
Và thay thế viewDidLoad bằng đoạn code sau
override func viewDidLoad() {
  super.viewDidLoad()

  setupView()

  populatePhotos()
}

Để implement handleRefresh() chúng ta có đoạn code

// 1 - load more photo khi bạn scroll 80% contentsize của scroll view
override func scrollViewDidScroll(scrollView: UIScrollView) {
  if scrollView.contentOffset.y + view.frame.size.height > scrollView.contentSize.height * 0.8 {
    populatePhotos()
  }
}

func populatePhotos() {
  // 2 - Chỉ load page hiện tại tránh load next page
  if populatingPhotos {
    return
  }

  populatingPhotos = true

  // 3 - return 50 photo cho mỗi request
  Alamofire.request(Five100px.Router.PopularPhotos(self.currentPage)).responseJSON() {
    (_, _, JSON, error) in

    if error == nil {
      // 4 - DISPATCH_QUEUE_PRIORITY_HIGH
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        // 5, 6, 7 - filter data
        let photoInfos = ((JSON as! NSDictionary).valueForKey("photos") as! [NSDictionary]).filter({ ($0["nsfw"] as! Bool) == false }).map { PhotoInfo(id: $0["id"] as! Int, url: $0["image_url"] as! String) }

        // 8 - collectionView update count number
        let lastItem = self.photos.count
        // 9 - add photoInfos to array photos
        self.photos.addObjectsFromArray(photoInfos)

        // 10 create NSIndexPath to insert CollectionView
        let indexPaths = (lastItem..<self.photos.count).map { NSIndexPath(forItem: $0, inSection: 0) }

        // 11 - Update UI use insertItemsAtIndexPaths
        dispatch_async(dispatch_get_main_queue()) {
          self.collectionView!.insertItemsAtIndexPaths(indexPaths)
        }

        self.currentPage++
      }
    }
    self.populatingPhotos = false
  }
}

Bạn build và run ứng dụng

SlowScroll-179x320.png

Nguồn tham khảo beginning-alamofire-tutorial

All Rights Reserved