Swift编写自己的API客户端

作者:Nick O'Neill,文章中代码文件下载
翻译:BNCoding, 如有错误欢迎指出。原文链接

就像很多iOS开发者一样,我也使用AFNetworking类库来处理所有的网络操作(在Swift中与之对应的是Alamofire)。并且因为这些类库的存在开发者会觉得这意味着自己做类似的工作是困难或者昂贵的。在以前确实是这样的!在iOS6和之前版本中NSURLConnetion执行或者封装成一个便于使用、节约时间的类库是很痛苦的过程。

事实上从iOS7版本的NSURLSession后,网络操作变的比以前简单多了,而且编写自己的API客户端能够简化哪些依赖性。如果这些不必要的依赖不足以说服你,那么试想一下加入第三方的类库代码可能会带来一些不必要的bug,或者是当你只需要使用一个大类库中的很少一部分功能的时候考虑到你应用的二进制大小。

一个简单的NSURLSession例子

虽然NSURLSession很简单,我们还是来看看下面这个简单的示例:

let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

let request = NSURLRequest(URL: NSURL(string: "http://yourapi.com/endpoint")!)

let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
    if let data = data {
        let response = NSString(data: data, encoding: NSUTF8StringEncoding)
        print(response)
    }
}
task.resume()

上面代码中三个最主要的组成中的第一个就是NSURLSession。在本文中它并不需要进行专门的配置。它会处理我们传递分配的所有的数据或者下载任务,并调用block块代码返回结果。

希望你早就熟悉NSURLRequest了,NSURLRequest包含了网络请求的细节包括:URL、方法、其它参数等等。最简单的配置就是指定一个URL链接,因为默认方法为GET

接下来我对最后一个也就是NSURLSessionDataTask进行一下分析。它包含了一个block代码(也就是Swift中的闭包),该段代码会在请求得到响应的时候触发执行。我们会获得三个可选的类型变量:包含响应得到的原始数据的NSData,从响应中得到的带有元数据的NSURLResponse对象,以及可能的错误NSError

你自己的API客户端

现在我们已经对NSURLSession有了基本的了解,接下来我们将这些基本的操作封装成一个简单的API客户端。

这里最重要的核心部分是:封装一个包含NSURLRequest和方法名称、并返回成功与否和成功后得到的JSON数据的简单数据作业。如果你的API返回的是XML结构的数据,只需要对响应的部分进行修改,其余部分是相同的。

注意我们检查代码成功响应部分的方式!

private func dataTask(request: NSMutableURLRequest, method: String, completion: (success: Bool, object: AnyObject?) -> ()) {
    request.HTTPMethod = method

    let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

    session.dataTaskWithRequest(request) { (data, response, error) -> Void in
        if let data = data {
            let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
            if let response = response as? NSHTTPURLResponse where 200...299 ~= response.statusCode {
                completion(success: true, object: json)
            } else {
                completion(success: false, object: json)
            }
        }
    }.resume()
}

接下来,我们将一些常用的请求封装到一个指定HTTP方法和completion块(闭包)的小方法里面。当我们将这些融合到一起的时候将变得更有意义。

private func post(request: NSMutableURLRequest, completion: (success: Bool, object: AnyObject?) -> ()) {
    dataTask(request, method: "POST", completion: completion)
}

private func put(request: NSMutableURLRequest, completion: (success: Bool, object: AnyObject?) -> ()) {
    dataTask(request, method: "PUT", completion: completion)
}

private func get(request: NSMutableURLRequest, completion: (success: Bool, object: AnyObject?) -> ()) {
    dataTask(request, method: "GET", completion: completion)
}

需要简化的最后一个功能是,创建一个带有我们需要发送数据的NSURLRequest。这样我们还可以将参数编码为表单数据并且当拥有权限时还可以进行授权。在不同API接口变化时,该方法的变化是最大的,但是该方法的职能依旧保持不变。

private func clientURLRequest(path: String, params: Dictionary<String, AnyObject>? = nil) -> NSMutableURLRequest {
    let request = NSMutableURLRequest(URL: NSURL(string: "http://api.website.com/"+path)!)
    if let params = params {
        var paramString = ""
        for (key, value) in params {
            let escapedKey = key.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())
            let escapedValue = value.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())
            paramString += "\(escapedKey)=\(escapedValue)&"
        }

        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.HTTPBody = paramString.dataUsingEncoding(NSUTF8StringEncoding)
    }

    if let token = token {
        request.addValue("Bearer "+token, forHTTPHeaderField: "Authorization")
    }

    return request
}

最后,我们可以通过我们的API客户端开始网络请求了。下面是一个简单的的登录请求,使用了POST方法并将电子邮件和密码作为该登录请求的参数。在这个方法里你可以访问广义上的是否成功的标记量success,并且还有可能包含相关数据的Dictionary对象。

func login(email: String, password: String, completion: (success: Bool, message: String?) -> ()) {
    let loginObject = ["email": email, "password": password]

    post(clientURLRequest("auth/local", params: loginObject)) { (success, object) -> () in
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            if success {
                completion(success: true, message: nil)
            } else {
                var message = "there was an error"
                if let object = object, let passedMessage = object["message"] as? String {
                    message = passedMessage
                }
                completion(success: true, message: message)
            }
        })
    }
}

在用户登录成功或者将返回数据写入一个struct但是调用方法还没有返回时,你可能希望回收、存储token令牌。对于这种具体的请求行为就需要小心对待了。

我们可以针对每一个请求的类在API里面编写这样的小功能,并且进行自定义为调用代码提供一个一致且简单的体验(可能是在视图控制器的某处)。我们不必担心那些编码值或者是NSURL对象的生成,因为我们这个简单的小封装已经很好的解决这些问题了。

下面是我喜欢这篇文章的一些理由:

Easy to reason about
我们抽离了一部分我们不感兴趣或者是重复部分,但是对于我们需要理解的Http概念的代码依旧进行了讲解:提交一个url、一个方法名称以及一个标记成功与否的变量和获得的数据。

Flexible for different APIs
创建一个URL请求是一个经常需要做的事情,而这件事又由于不同人写的服务端代码的不同而需要进行调整修改。通常情况下作为移动开发者我并不能控制服务器具体的实现,所以可以进行自定义是每个项目的关键。

Short and sweet
这个基础的API客户端代码低于50行。如果我开始写一个很重量级的库去替代依赖性时,那将是件很让人崩溃事情,尤其当它还是一个工具时我永远都不会对它再进行任何修改。而这个API客户端是简单短小的,你可以频繁的对它进行修改和添加新方法。


BigNerdCoding
1.2k 声望125 粉丝

个人寄语: