Alamofire - Elegant networking in swift Part 2: Implement alamofire in the real project

Hôm này mình xin phép tiếp tục seri về Alamofire - thư viện phổ biến được viết cho iOSOSX sử dụng ngôn ngữ Swift. Trong part 1 của seri này mình có viết về Alamofire, nó là gì, sử dụng thế nào... thì part 2 trong seri này mình sẽ hướng dẫn các bạn cách viết một network layer tách biệt (điều rất quan trọng khi bạn tham gia một dự án dù lớn hay nhỏ và nếu bạn là một người muốn viết code dễ hiểu, dễ maintain...)

Các bạn có thể tham khảo part 1 tại Part 1: Getting started

Trong part 2 này mình có sử dụng Alamofire để viết một netwwork layer tách biệt, sử dụng mô hình VIPER (VIEW - INTERACTOR - PRESENTER - ENTITY - ROUTING) đó là một mô hình được áp dụng khá phổ biến, code trong sáng, dễ hiểu, dễ maintain, nếu là một iOS developer thì thật tiếc nếu bạn không biết về nó Mô hình VIPER

1. Tạo single view application

  • Mở Xcode từ thư mục /Applications
  • Một cửa sổ xuất hiện

2_welcomewindow_2x.png

  • Bạn cũng có thể tạo một single application theo các khác

Screen Shot 2016-02-27 at 22.17.33.png

Screen Shot 2016-02-27 at 22.17.54.png New/Project/Single View Application

  • Đặt tên cho project (AlamofireNetworkLayer)

2_newproject_2x.png

2. Sử dụng cocoa pod

  • Cocoapod là gì? Cocoapod đơn giản là nơi quản lý các thư viện của bên thứ 3 sử dụng cho các dự án Swift và Objecttive C. Nếu như trước đây để có thể sử dụng được các thư viện từ bên thứ 3 thì developer phải tải trực tiếp từ github hoặc một nguồn nào đó về và cấu hình PATH tới thư viện đó, giờ đây khi có Pod thì developer không còn phải làm việc đó nữa, chỉ cần cài đặt và cấu hình Pod với một vài thao tác đơn giản. Bạn có thể tham khảo theo đường dẫn sau Cocoa pod
  • Đầu tiên bạn cần cài gem sử dụng command line
$ sudo gem install cocoapods
  • Sau khi cài đặt thành công bạn cần khởi tạo pod cho dự án của bạn. Mở command line và đi tới thư mục project của bạn rồi thực hiện
$ pod init
  • Vậy là pod của bạn được tạo ra. Bước tiếp theo bạn thực hiện lệnh sau để tạo ra một workspace khác
$ pod install
  • Khi pod install được thực hiện xong thì tại thư mục project của bạn sẽ có thể một workspace khác (màu icon khác với project bình thường lúc ban đầu tạo). Bạn đóng project hiện tại và mở workspace vừa tạo ra.
  • Trong bài viết này tôi có sử dụng 2 thư viện là AlamofireSwiftJSON nên nội dung file pod của tôi như sau:
platform :ios, '8.0'
use_frameworks!
target 'AlamofireNetworkLayer' do
    pod 'Alamofire'
    pod 'SwiftyJSON'
end
target 'AlamofireNetworkLayerTests' do
end
target 'AlamofireNetworkLayerUITests' do
end
  • Bước cuối để có thể sử dụng các thư việc đó bạn cần thực hiện lệnh sau để tự động tải về và cấu hình PATH
$ pod install

3. Mô hình VIPER

  • Mình sẽ không nói sâu về mô hình này là gì và như thế nào. Các bạn có thể đọc tại Mô Hình VIPER

  • Khi có thể hiểu và áp dụng nó vào thực tế bạn sẽ thấy sức mạnh của mô hình này

  • Trong bài viết này mình sẽ hướng dẫn các bạn viết theo mô hình VIPER. Trước tiên cần hiểu VIPER là viết tắt của từ gì?

    V: View (ViewController và các view control)

    I: Interactor (thành phần tương tác với data)

    P: Prenseter (Thành phần chính tương tác với View, Interactor và Routing)

    E: Entity (các thực thể)

    R: Routing (thực hiện nhiệm vụ chuyển đổi giữa các ViewController)

  • Vậy mình bắt đầu tạo các file có tên theo cấu trúc thư mục sau:

Screen Shot 2016-02-27 at 23.48.58.png

  • Tạo action từ một button như sau:

Screen Shot 2016-02-27 at 23.31.02.png

Tương ứng với action đó trong ViewController có nội dung như sau:

//
//  ViewController.swift
//  AlamofireNetworkLayer
//
//  Created by ngodacdu on 2/27/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//
import UIKit
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func didTouchUpInSideSearchButton(sender: AnyObject) {

    }
}
  • Tạo interface (TFInterface) có nội dung như sau
//
//  TFInterface.swift
//  AlamofireNetworkLayer
//
//  Created by ngodacdu on 2/27/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//
import UIKit
protocol TFInterface {
    func didTouchUpInSideSearchButton(searchText: String)
}

Mục đích: func didTouchUpInSideSearchButton() được thực hiện khi người dùng nhấn vào action

  • Tạo presenter (TFPresenter) có nội dung
//
//  TFPresenter.swift
//  AlamofireNetworkLayer
//
//  Created by ngodacdu on 2/27/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//
import UIKit
class TFPresenter: NSObject, TFInterface {
    func didTouchUpInSideSearchButton(searchText: String) {

    }
}
  • Tạo một routing mục đích chuyển sang màn hình DetailViewController sau khi bạn lấy xong dữ liệu SearchLocation có nội dung như sau:
//
//  TFRouting.swift
//  AlamofireNetworkLayer
//
//  Created by ngodacdu on 2/27/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//

import UIKit

class TFRouting: NSObject {

    var viewController: ViewController?

    class var shareRouting: TFRouting {
        struct Static {
            static let instance = TFRouting()
        }
        return Static.instance
    }

    private func mainStoryboard() -> UIStoryboard {
        return UIStoryboard(name: "Main", bundle: nil)
    }

    func pushDetailViewController() {
        let detailViewController = mainStoryboard().instantiateViewControllerWithIdentifier("DetailViewController")
        if let currentDetailVC = detailViewController as? DetailViewController {
            viewController?.navigationController?.pushViewController(
                currentDetailVC,
                animated: true
            )
        }
    }

}
  • Tạo một Interactor (TFInteractor)
//
//  TFInteractor.swift
//  AlamofireNetworkLayer
//
//  Created by ngodacdu on 2/27/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//

import UIKit

protocol TFInteractorOutput {
    func searchApiDidFinished(data: TFSearchLocationDto)
    func searchApiDidError(error: NSError)
}

protocol TFInteractorInput {
    func requestSearchApi(searchText: String)
}

class TFInteractor: NSObject, TFInteractorInput {

    var output: TFInteractorOutput?

    func requestSearchApi(searchText: String) {

    }

}
  • Tạo Model, trong bài viết này mình không đi sâu vào các properties của phần model này nên các bạn có thể tham khảo trong project ở phần cuối bài.
  • Trong bài viết này mình có sử dụng KEY và API về thông tin thời tiết của một địa điểm mà mình tìm kiếm. Các bạn có thể tham khảo worldweatheronline. Sau khi đọc bài viết xong bạn hãy tự tạo cho mình một KEY free và tự tìm hiểu các properties mình đã sử dụng trong phần Model nhé.
  • Để project có thể hoạt động được thì chúng ta cần phải thực hiện setup vài thứ sau khi đã có tầng network.

4. Tạo network layer sử dụng Alamofire

  • Tạo Api Path
//
//  TFApiPath.swift
//  todayweather
//
//  Created by ngodacdu on 2/21/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//

import Foundation
import Alamofire

/*
    - Bạn cần có một base url
    - Key bạn tạo từ website
*/
let URL = "http://api.worldweatheronline.com"
let WEATHER_ONLINE_KEY_API = "WorldWeatherKey"

/*
    - Số kết quả tìm kiếm trả về lớn nhất
    - Số ngày bạn muốn có thông tin thời tiết
*/
let SEARCH_NUM_RESULT = 5
let NUM_OF_DAY = 5

/*
    - Base URL
    - Path
    - Method
*/
public protocol TargetType {
    var baseURL: String { get }
    var path: String { get }
    var method: String { get }
}

private extension String {
    var URLEscapedString: String {
        return self.stringByAddingPercentEncodingWithAllowedCharacters(
            NSCharacterSet.URLHostAllowedCharacterSet()
        )!
    }
}

/*
    Gồm 2 API là search api và location weather api
*/
public enum API {
    case Search(String)
    case LocationWeather(String, String)
}

extension API: TargetType {

    public var baseURL: String {
        return URL
    }

    public var path: String {
        switch self {
        case .Search(let searchString):
            let query = "?query=\(searchString.stringByReplacingOccurrencesOfString(" ", withString: "+"))"
            let num = "&num_of_results=\(SEARCH_NUM_RESULT)"
            let format = "&format=json"
            let key = "&key=\(WEATHER_ONLINE_KEY_API)"
            return "\(baseURL)/free/v2/search.ashx" + query + num + format + key
        case .LocationWeather(let lat, let lon):
            let q = "?q=\(lat),\(lon)"
            let num = "&num_of_days=\(NUM_OF_DAY)"
            let format = "&format=json"
            let key = "&key=\(WEATHER_ONLINE_KEY_API)"
            return "\(baseURL)/free/v2/weather.ashx" + q + num + format + key
        }
    }

    public var method: String {
        switch self {
        case .Search:
            return "GET"
        case .LocationWeather:
            return "GET"
        }
    }

}

  • Các bạn có thể parser data trả về theo cách sau (tham khảo)
//
//  TFJsonParser.swift
//  todayweather
//
//  Created by ngodacdu on 2/21/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//

import Foundation
import SwiftyJSON

class SSJsonParser: NSObject {

    class var sharedInstance : SSJsonParser {
        struct Static {
            static let instance : SSJsonParser = SSJsonParser()
        }
        return Static.instance
    }

    func parseJson(api : API, json : JSON) -> Any? {
        switch api {
        case .Search(_):
            return parserSearchLocationApi(json)
        case .LocationWeather(_, _):
            return parserLocationWeatherApi(json)
        }
    }

    //MARK: Search Location Api
    private func parserSearchLocationApi(json: JSON) -> [TFSearchLocationDto] {
        var searchLocations: [TFSearchLocationDto] = [TFSearchLocationDto]()
        if let searchApi: JSON = json["search_api"] {
            if let results = searchApi["result"].array {
                for result in results {
                    let searchLocation = TFSearchLocationDto()
                    if let firstAreaName: JSON = result["areaName"].array?.first {
                        searchLocation.areaName = firstAreaName["value"].string
                    }
                    if let firstRegion: JSON = result["region"].array?.first {
                        searchLocation.region = firstRegion["value"].string
                    }
                    if let firstWeatherUrl: JSON = result["weatherUrl"].array?.first {
                        searchLocation.weatherUrl = firstWeatherUrl["value"].string
                    }
                    searchLocation.latitude = result["latitude"].string
                    searchLocation.longitude = result["longitude"].string
                    if let firstCountry: JSON = result["country"].array?.first {
                        searchLocation.country = firstCountry["value"].string
                    }
                    searchLocation.population = result["population"].string
                    searchLocations.append(searchLocation)
                }
            }
        }
        return searchLocations
    }

    //MARK: Location Weather Api
    private func parserLocationWeatherApi(json: JSON) -> TFLocationWeatherDto {
        let locationWeatherDto = TFLocationWeatherDto()
        let data: JSON = json["data"]
        locationWeatherDto.dateWeathers = parserDateWeather(data["weather"])
        if let currentCondition = data["current_condition"].array?.first {
            locationWeatherDto.precipMM = currentCondition["precipMM"].string
            locationWeatherDto.temp_F = currentCondition["temp_F"].string
            locationWeatherDto.winddir16Point = currentCondition["winddir16Point"].string
            locationWeatherDto.winddirDegree = currentCondition["winddirDegree"].string
            locationWeatherDto.humidity = currentCondition["humidity"].string
            locationWeatherDto.cloudcover = currentCondition["cloudcover"].string
            locationWeatherDto.feelsLikeF = currentCondition["FeelsLikeF"].string
            locationWeatherDto.weatherCode = currentCondition["weatherCode"].string
            if let weatherIconUrl = currentCondition["weatherIconUrl"].array?.first {
                locationWeatherDto.weatherIconUrl = weatherIconUrl["value"].string
            }
            locationWeatherDto.temp_C = currentCondition["temp_C"].string
            locationWeatherDto.windspeedKmph = currentCondition["windspeedKmph"].string
            locationWeatherDto.windspeedMiles = currentCondition["windspeedMiles"].string
            locationWeatherDto.visibility = currentCondition["visibility"].string
            locationWeatherDto.feelsLikeC = currentCondition["FeelsLikeC"].string
            locationWeatherDto.observation_time = currentCondition["observation_time"].string
            if let weatherDesc = currentCondition["weatherDesc"].array?.first {
                locationWeatherDto.weatherDesc = weatherDesc["value"].string
            }
            locationWeatherDto.pressure = currentCondition["pressure"].string
        }
        return locationWeatherDto
    }

    private func parserDateWeather(json: JSON) -> [TFDateWeatherDto] {
        var dateWeathers: [TFDateWeatherDto] = [TFDateWeatherDto]()
        if let jsonArray = json.array {
            for dateJson in jsonArray {
                let dateWeather: TFDateWeatherDto = TFDateWeatherDto()
                dateWeather.date = dateJson["date"].string
                dateWeather.maxtempC = dateJson["maxtempC"].string
                dateWeather.mintempC = dateJson["mintempC"].string
                dateWeather.mintempF = dateJson["mintempF"].string
                dateWeather.hourlys = parserHourWeather(dateJson["hourly"])
                dateWeather.uvIndex = dateJson["uvIndex"].string
                dateWeather.astronomy = parserAstronomy(dateJson["astronomy"])
                dateWeather.maxtempF = dateJson["maxtempF"].string
                dateWeathers.append(dateWeather)
            }
        }
        return dateWeathers
    }

    private func parserHourWeather(json: JSON) -> [TFHourWeatherDto] {
        let hourWeatherDtos: [TFHourWeatherDto] = [TFHourWeatherDto]()
        if let jsonArray = json.array {
            for hourJson in jsonArray {
                let hourWeatherDto = TFHourWeatherDto()
                hourWeatherDto.winddirDegree = hourJson["winddirDegree"].string
                hourWeatherDto.pressure = hourJson["pressure"].string
                if let weatherIconUrl = hourJson["weatherIconUrl"].array?.first {
                    hourWeatherDto.weatherIconUrl = weatherIconUrl["value"].string
                }
                hourWeatherDto.chanceofremdry = hourJson["chanceofremdry"].string
                hourWeatherDto.windGustMiles = hourJson["WindGustMiles"].string
                hourWeatherDto.windChillF = hourJson["WindChillF"].string
                hourWeatherDto.visibility = hourJson["visibility"].string
                hourWeatherDto.time = hourJson["time"].string
                hourWeatherDto.dewPointF = hourJson["DewPointF"].string
                hourWeatherDto.chanceofsunshine = hourJson["chanceofsunshine"].string
                hourWeatherDto.chanceofrain = hourJson["chanceofrain"].string
                hourWeatherDto.chanceofwindy = hourJson["chanceofwindy"].string
                hourWeatherDto.windspeedMiles = hourJson["windspeedMiles"].string
                hourWeatherDto.chanceoffog = hourJson["chanceoffog"].string
                hourWeatherDto.windGustKmph = hourJson["WindGustKmph"].string
                hourWeatherDto.chanceofsnow = hourJson["chanceofsnow"].string
                hourWeatherDto.tempF = hourJson["tempF"].string
                hourWeatherDto.windspeedKmph = hourJson["windspeedKmph"].string
                hourWeatherDto.windChillC = hourJson["WindChillC"].string
                hourWeatherDto.heatIndexF = hourJson["HeatIndexF"].string
                hourWeatherDto.precipMM = hourJson["precipMM"].string
                hourWeatherDto.weatherCode = hourJson["weatherCode"].string
                hourWeatherDto.chanceofthunder = hourJson["chanceofthunder"].string
                if let weatherDesc = hourJson["weatherDesc"].array?.first {
                    hourWeatherDto.weatherDesc = weatherDesc["value"].string
                }
                hourWeatherDto.chanceofhightemp = hourJson["chanceofhightemp"].string
                hourWeatherDto.feelsLikeF = hourJson["FeelsLikeF"].string
                hourWeatherDto.chanceoffrost = hourJson["chanceoffrost"].string
                hourWeatherDto.winddir16Point = hourJson["winddir16Point"].string
                hourWeatherDto.heatIndexC = hourJson["HeatIndexC"].string
                hourWeatherDto.dewPointC = hourJson["DewPointC"].string
                hourWeatherDto.tempC = hourJson["tempC"].string
                hourWeatherDto.chanceofovercast = hourJson["chanceofovercast"].string
                hourWeatherDto.humidity = hourJson["humidity"].string
                hourWeatherDto.feelsLikeC = hourJson["FeelsLikeC"].string
                hourWeatherDto.cloudcover = hourJson["cloudcover"].string
            }
        }
        return hourWeatherDtos
    }

    private func parserAstronomy(json: JSON) -> TFAstronomyWeatherDto {
        let astronomy = TFAstronomyWeatherDto()
        astronomy.moonset = json["moonset"].string
        astronomy.moonrise = json["moonrise"].string
        astronomy.sunset = json["sunset"].string
        astronomy.sunrise = json["sunrise"].string
        return astronomy
    }

}

  • Tạo network sử dụng alamofire
//
//  TFNetworkLayer.swift
//  todayweather
//
//  Created by ngodacdu on 2/21/16.
//  Copyright © 2016 ngodacdu. All rights reserved.
//

import Foundation
import Alamofire
import SwiftyJSON

typealias NetworkStartHandler = () -> ()
typealias NetworkErrorHandler = (NSError) -> ()
typealias NetworkFishHandler  = (Any?) -> ()

class SSNetworkLayer: NSObject {

    var start: NetworkStartHandler?
    var error: NetworkErrorHandler?
    var finish: NetworkFishHandler?

    var request: Request?
    var api: API!
    var parameters: [String : AnyObject]?
    var headers: [String : String]?

    func setStartHandler(start : NetworkStartHandler) {
        self.start = start
    }

    func setErrorHandler(error : NetworkErrorHandler) {
        self.error = error
    }

    func setFinishHandler(finish : NetworkFishHandler) {
        self.finish = finish
    }

    private func queryStringFromDictionary(parameters: [String : AnyObject]) -> String  {
        var parts: Array<String> = [String]()
        for (key,value) in parameters {
            let part: String = String(format: "%@=%@", key,value.description)
            parts.append(part)
        }
        let joinedString = (parts as NSArray).componentsJoinedByString("&")
        return joinedString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLPathAllowedCharacterSet())!
    }

    private func getMethod(api: API) -> Alamofire.Method {
        let requestMethod: Alamofire.Method
        switch api.method {
        case "POST":
            requestMethod = .POST
            break
        case "PUT":
            requestMethod = .PUT
            break
        default:
            requestMethod = .GET
            break
        }
        return requestMethod
    }

    //MARK: Request URL
    func requestApi(api api: API, parameters: [String : AnyObject]?, headers: [String : String]?) {
        self.api = api
        self.parameters = parameters
        self.headers = headers

        start?()

        Alamofire.request(
            getMethod(api),
            api.path,
            parameters: parameters,
            encoding: .JSON,
            headers: headers
            ).responseJSON { (response : Response<AnyObject, NSError>) -> Void in
                switch response.result {
                case .Success(let json):
                    let response = SSJsonParser.sharedInstance.parseJson(api, json: JSON(json))
                    self.finish?(response)
                    break
                case .Failure(let error):
                    self.error?(error)
                    break
                }
        }
    }

}

Vậy là chúng ta đã tạo xong được tầng network

5. Setup project để hoạt động

  • ViewController thực hiện action
    var handler: TFInterface?

    @IBAction func didTouchUpInSideSearchButton(sender: AnyObject) {
        handler?.didTouchUpInSideSearchButton("Hanoi")
    }
  • Presenter thực hiện action
class TFPresenter: NSObject, TFInterface {

    var input: TFInteractorInput?

    func didTouchUpInSideSearchButton(searchText: String) {
        input?.requestSearchApi(searchText)
    }

}

extension TFPresenter: TFInteractorOutput {

    func searchApiDidFinished(data: TFSearchLocationDto) {

    }

    func searchApiDidError(error: NSError) {

    }

}
  • Interactor implement network layer
import UIKit

protocol TFInteractorOutput {
    func searchApiDidFinished(data: TFSearchLocationDto)
    func searchApiDidError(error: NSError)
}

protocol TFInteractorInput {
    func requestSearchApi(searchText: String)
}

class TFInteractor: NSObject, TFInteractorInput {

    var output: TFInteractorOutput?

    func requestSearchApi(searchText: String) {
        let network = SSNetworkLayer()
        weak var weakSelf = self
        network.setFinishHandler { (result) -> () in
            if let data = result as? TFSearchLocationDto {
                weakSelf?.output?.searchApiDidFinished(data)
            }
        }
        network.setErrorHandler { (error) -> () in
            weakSelf?.output?.searchApiDidError(error)
        }
        network.requestApi(
            api: .Search(searchText),
            parameters: nil,
            headers: nil
        )
    }

}
  • Routing thực hiện push sang DetailViewController
import UIKit

class TFRouting: NSObject {

    var viewController: ViewController?

    class var shareRouting: TFRouting {
        struct Static {
            static let instance = TFRouting()
        }
        return Static.instance
    }

    private func mainStoryboard() -> UIStoryboard {
        return UIStoryboard(name: "Main", bundle: nil)
    }

    func pushDetailViewController() {
        let detailViewController = mainStoryboard().instantiateViewControllerWithIdentifier("DetailViewController")
        if let currentDetailVC = detailViewController as? DetailViewController {
            viewController?.navigationController?.pushViewController(
                currentDetailVC,
                animated: true
            )
        }
    }

}
  • Trong AppDelegate thực hiện chuẩn bị
private func setupViewController() {
        if let rootViewController = window?.rootViewController as? ViewController {
            let presenter = TFPresenter()
            let interactor = TFInteractor()
            interactor.output = presenter
            presenter.input = interactor
            rootViewController.handler = presenter
        }
    }

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        setupViewController()

        return true
    }

Vậy là bạn đã hoàn thành project có sử dụng Alamofire và mô hình VIPER.

Ngoài ra nếu thắc mắc nào bạn có thể comment bên dưới hoặc tham khảo project tại Alamofire - Elegant networking in swift Part 2: Implement alamofire in the real project

All Rights Reserved