Swift: 用Alamofire做http請求,用ObjectMapper解析JSON

小紅星閃啊閃發表於2016-07-27
示例程式碼看最後。

跟不上時代的人突然間走在了時代的前列,果然有別樣的風景。首先鄙視一下AFNetworking。這個東西實在太難用了。不想封裝都不行,要不寫一大堆程式碼。

NSURL *URL = [NSURL URLWithString:@"http://example.com/resources/123.json"];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:URL.absoluteString parameters:nil 
    progress:nil 
    success:^(NSURLSessionTask *task, id responseObject) { 
        NSLog(@"JSON: %@", responseObject);
    } 
    failure:^(NSURLSessionTask *operation, NSError *error) {  
        NSLog(@"Error: %@", error);
    }
];

Http請求

但是用alamofire就簡單的很多了,如:

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]) 
    .response { request, response, data, error in
         print(response) 
    }

都是一個GET請求,但是可見的是Alamofire程式碼量少很多。這也是和AFNetworking3.x比較了,如果你用的是AFNetworking2.x的話程式碼量的對比更加明顯。對於程式設計師來說呼叫方法的API簡單方便就是使用者體驗。Developer們也是需要滿足UE的需要的。

下面開始進入正題。下面用請求微博的time line來做栗子。

parameters = ["access_token": weiboUserInfo.accessToken ?? "",  "source": ConstantUtil.WEIBO_APPKEY] //1
Alamofire.request(.GET, "https://api.weibo.com/2/statuses/friends_timeline.json" //2
    , parameters: parameters, encoding: .URL, headers: nil)
    .responseString(completionHandler: {response in
        print("response:- (response)") //3
})

這裡用Alamofire請求微博的time line。

  1. 請求微博的time line就需要SSO或者網頁方式登入微博之後從伺服器返回的access_token。另外一個必須的輸入引數就是新增微博應用的時候生成的app key。

  2. https://api.weibo.com/2/statu…請求的url。
    這個url返回的就是你follow的好友的微博。就是你一開啟微部落格戶端看到的那些。

  3. 我們知道Alamofire可以把請求返回的資料轉化為JSON、String和NSData。如果是作為JSON來處理,也就是使用了responseJSON 方法的話,JSON資料會被自動轉化為NSDictionary。我們後面需要用到字串來實現json字串和Model物件的匹配,所以我們用方法responseString

如果一切設定正確,你會看到這樣的結果:

{
    "statuses": [
        {
            "created_at": "Tue May 31 17:46:55 +0800 2011",
            "id": 11488058246,
            "text": "求關注。",
            "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>",
            "favorited": false,
            "truncated": false,
            "in_reply_to_status_id": "",
            "in_reply_to_user_id": "",
            "in_reply_to_screen_name": "",
            "geo": null,
            "mid": "5612814510546515491",
            "reposts_count": 8,
            "comments_count": 9,
            "annotations": [],
            "user": {
                "id": 1404376560,
                "screen_name": "zaku",
                "name": "zaku",
                "province": "11",
                "city": "5",
                "location": "北京 朝陽區",
                "description": "人生五十年,乃如夢如幻;有生斯有死,壯士復何憾。",
                "url": "http://blog.sina.com.cn/zaku",
                "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1",
                "domain": "zaku",
                "gender": "m",
                "followers_count": 1204,
                ...
            }
        },
        ...
    ],
    "ad": [
        {
            "id": 3366614911586452,
            "mark": "AB21321XDFJJK"
        },
        ...
    ],
    "previous_cursor": 0,      // 暫時不支援
    "next_cursor": 11488013766,     // 暫時不支援
    "total_number": 81655
}

以上是微博給出來的例子的一部分,我們來看看我們需要什麼。我們需要一部分文字和一部分的圖片。之後要顯示的內容主要就是文字或者圖片。

解析

我們用ObjectMapper解析json。ObjectMapper是一個雙向的轉化工具。可以把json字串轉化成model也可以把model轉化成json字串。

安裝ObjectMapper:

pod `ObjectMapper`, `~> 1.1`

ObjectMapper對於json的解析都是從外往內進行的,這個層層解析的過程中一般沒有特殊指定的話每一層都不能少(可以通過制定解析路徑減少)。每一層都需要配備一個實體類。

最外面的一層是:

{
    "statuses": [
      ...
    ],
    "previous_cursor": 0,      
    "next_cursor": 11488013766,     
    "total_number": 81655
}

所以對應的model定義是這樣的:

import ObjectMapper

class BaseModel: Mappable {  // 1
    var previousCursor: Int?
    var nextCursor: Int?
    //var statuses 
    var totalNumber: Int?

    required init?(_ map: Map) {  // 2
        
    }
    
    func mapping(map: Map) { // 3
        previousCursor <- map["previous_cursor"]
        nextCursor <- map["next_cursor"]
        //hasVisible <- map["hasvisible"]
        statuses <- map["..."] // 4
        totalNumber <- map["total_number"]
    }
}

最重要的是先import ObjectMapper。沒有這個什麼都幹不了。

  1. BaseModel類需要實現Mappable介面。後面就是這個protocol的實現。

  2. 返回可能為空物件的初始化方法,法暫時用不到。

  3. 這個方法最關鍵了。在這個方法裡指定json的值對應的是model裡的哪個屬性。這部分功能可以自動實現,哪位有心人可以fork出來寫一個,也方便大家使用

  4. 請看下文。

在深入一層

上問的標籤4的內容我們在這裡詳細介紹。我們要展示的內容都是在statuses下的。那麼我們應該如何處理這部分的內容呢?statuses的json格式是這樣的:

{
    "statuses": [
      {
          "created_at": "Tue May 31 17:46:55 +0800 2011",
           "id": 11488058246,
           "text": "求關注。",
           "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>",
           "favorited": false,
           "truncated": false,
           "in_reply_to_status_id": "",
           "in_reply_to_user_id": "",
           "in_reply_to_screen_name": "",
           "geo": null,
          ...
      }
    ],
}

可以有兩個方式來處理深層的json資料。一個是在mapping方法裡指定json資料和屬性的對應關係。比如在BaseMode類中對映statuses中的text可以這樣寫:

class BaseModel {
  var text: String?

  required init?(_ map: Map) { 
  }

  func mapping(map: Map) {
    self.text <- map["statuses.text"]
  }
}

但是這樣是錯誤的!因為statuses是一個陣列,而不是一個物件。只有statuses對應的是一個物件的時候才適用於這個情況。

對上面的程式碼進行修改,讓其適用於資料的情況。

class BaseModel {
  var text: String?

  required init?(_ map: Map) { 
  }

  func mapping(map: Map) {
    self.text <- map["status.0.text"]
  }
}

self.text <- map["statuses.0.text"]中間的數字說明text屬性對應的是json中的statuses陣列的第一個元素的text的值。但是在statuses下會有很多個json物件,一個一個的挨個解析的方式顯然是不適合的。更不用說這才兩層,有多少奇葩的API返回的是三層甚至更多的?

那麼就剩下最後的一種方法了。內層json的model類繼承外層的json的model類。按照這個方法那麼我們為statuses對應的json物件定義一個model類為StatusModel。由於StatusModel對應的是內層的json物件,那麼就需要繼承外層的json物件的類,也就是BaseModel。剛開始就命名為BaseModel應該是已經露餡了。

class StatusModel: BaseModel { // 1
    var statusId: String?
    var thumbnailPic: String?
    var bmiddlePic: String?
    var originalPic: String?
    var weiboText: String?
    var user: WBUserModel?

    required init?(_ map: Map) {
        super.init(map)  // 2
        
    }
    
    override func mapping(map: Map) {
        super.mapping(map) // 2
        statusId <- map["id"]
        thumbnailPic <- map["thumbnail_pic"]
        bmiddlePic <- map["bmiddle_pic"]
        originalPic <- map["original_pic"]
        weiboText <- map["text"]
    }
}
  1. 也就是我們說的json物件巢狀時的model類的繼承關係。

  2. 在這種繼承關係中需要十分注意的是。在Mappable協議的方法的呼叫中需要先呼叫基類的對應方法,super.init(map) super.mapping(map)。至於說mapping 方法的對映關係,每個json物件對應的model類只管這一個物件的就可以。

那麼在最外層的BaseModel類中的statuses屬性也就可以給出一個正確的完整的寫法了。

class BaseModel: Mappable {
    var previousCursor: Int?
    var nextCursor: Int?
    var hasVisible: Bool?
    var statuses: [StatusModel]? // 1
    var totalNumber: Int?
    
    required init?(_ map: Map) {
        
    }
    
    func mapping(map: Map) {
        previousCursor <- map["previous_cursor"]
        nextCursor <- map["next_cursor"]
        hasVisible <- map["hasvisible"]
        statuses <- map["statuses"]  // 2
        totalNumber <- map["total_number"]
    }
}
  1. 內層的statuses陣列直接呼叫內層json物件對應的model類的陣列,也即是 var statuses: [StatusModel]?

  2. mapping方法中指定屬性和json物件的關係,這裡是statuses <- map["statuses"]

這樣ObjectMapper就知道應該如何解析json字串到對應的類物件中了。除了上面提到的,ObjectMapper還有很多其他的功能。如果需要了解更多可以檢視官方文件

那麼從http請求,到返回資料,到解析json串的一系列動作就可以完整的聯結起來了。最開始介紹使用Alamofire請求併成功返回之後,我們只是把字串列印了出來。現在可以呼叫map方法來匹配json串和我們定義好的model類了。

parameters = ["access_token": weiboUserInfo.accessToken ?? "",
                          "source": ConstantUtil.WEIBO_APPKEY]
            Alamofire.request(.GET, "https://api.weibo.com/2/statuses/friends_timeline.json", parameters: parameters, encoding: .URL, headers: nil)
                .responseString(completionHandler: {response in
                    print("response:- (response)") 
                    let statuses = Mapper<BaseModel>().map(response.result.value) // 1
                    print("total number: (statuses!.totalNumber)")
                    if let timeLine = statuses where timeLine.totalNumber > 0 { // 2
                        self.timeLineStatus = timeLine.statuses
                        self.collectionView?.reloadData()
                    }
            })
  1. 使用Mapper<BaseModel>().map(response.result.value)方法來對映json串。這裡需要分開來看。Mapper<BaseModel>()初始化了一個Mapper物件。Mapper是一個泛型,型別引數就是我們定義的最外層的json物件對應的model類BaseModel。之後我們呼叫了這個初始化好的Mapper物件的map方法。這個方法的引數就是一個json串,也就是字串型別的,但是這個字串必須是json格式的。response.result.value取出了http請求之後返回的json串。

  2. map方法返回的是可空型別的。所以需要用if-let的方式檢查一下返回的值是否可用。在可用的情況下用where語句判斷返回的timeLine總數是否大於零。大於零才是有意義的,才重新整理collection view。

示例程式碼在這裡。這裡沒有使用微博的API,而是用了Github的API來演示請求和JSON處理。比較簡單。不過Github奇葩的返回的結果就是一個JSON Array,居然可以使用ObjectMapper的mapArray方法一次搞定。這算是一個小坑。其他的都很常規了。

to be continued…

相關文章