iOS之Swinject入门教程

Swinject Tutorial for iOS: 入门教程

我们通过一个简短教程来探索Dependency Injection (DI),主要介绍一款Swift语言写的框架——Swinject。

在本教程中,您将通过Swinject探索依赖注入(DI)。通过改进一个名为Bitcoin Adventurer(Bitcoin冒险家)的iOS小程序来实现这一点,该程序可以显示当前比特币的价格。在阅读本教程时,您将重构应用程序,并完成单元测试。

依赖注入(DI)是一种组织代码的方法,目的使其依赖项由其他不同的对象提供,而不是由其本身提供,通过本教程的代码演示更容易理解一些。使用依赖注入技术可以让代码耦合更松散,便于单元测试和重构。

实现依赖注入技术并不一定非要使用第三方库来实现,但是使用Swinject可以让工作更简单,即使在代码复杂度不断升高时。

为什么使用依赖注入?

依赖注入技术依赖于一种称为控制反转的原理。其主要思想是,一段需要依赖关系的代码不会为自己创建依赖关系,而是将提供这些依赖关系的控制权交给更高的抽象对象。这些依赖关系通常被传递到对象的初始化代码中。

使用了DI框架中流行的模式:依赖项注入(DI)容器。这种模式使依赖项的解析变得简单,即使代码复杂度增加。

在代码实践中,控制反转的主要好处是代码更改仍然是独立的。一个DI Container提供某些对象来支持控制主体倒置,而这些对象本身知道如何提供依赖关系。你所需要做的就是向容器请求你需要的这些对象! 听起来很难理解的样子,下面来看看代码实例。

开始吧

这里下载项目代码(链接地址)。打开Bitcoin Adventurer.xcworkspace,然后按Command+R运行项目。

当应用程序启动时,你会看到屏幕上显示的比特币的当前价格。点击Refresh会发出HTTP请求来检索最新的数据,这些数据被记录到Xcode控制台。比特币是一种易波动的加密货币,其价值经常波动,因此Coinbase API大约每30秒就有一个新的比特币价格可用。

您可能还注意到控制台记录了一个错误。您现在可以忽略它,因为您将在本教程的后面介绍它。

返回Xcode检查项目:

  • 这个app包含一个UIViewController, BitcoinViewController,在main.storyboard中引用。
  • 所有的网络层和数据逻辑层都位于BitcoinViewController.swift中。按照目前的代码,独立于UIViewController生命周期测试逻辑是很困难的,因为视图层与它的底层逻辑和依赖关系高度耦合。
  • 我们已经通过CocoaPods为您添加了一个依赖项Swinject。目前它还没有在你的任何Swift文件中使用,但这即将改变!

依赖注入DI和解耦合

如前所说的依赖关系定义,就是为另一个对象完成工作的一段代码,最好是由单独的对象提供的,或者说是注入一段代码来完成依赖关系建立。

来探索下Bitcoin Adventurer项目代码中的依赖关系。

打开BitcoinViewController.swift,可以看到有三个主要职责:网络请求数据解析格式化

网络请求和数据解析

大部分网络请求任务通过一个函数requestPrice()

private func requestPrice()  {
let bitcoin = Coinbase.bitcoin.path

// 1. Make URL request 创建URL
guard let url = URL(string: bitcoin) else { return }
var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringCacheData

// 2. Make networking request 发送网络请求
let task = URLSession.shared.dataTask(with: request) { data, _, error in

// 3. Check for errors 检测错误
if let error = error {
print("Error received requesting Bitcoin price: \(error.localizedDescription)")
return
}

// 4. Parse the returned information 解析网络返回数据
let decoder = JSONDecoder()

guard let data = data,
let response = try? decoder.decode(PriceResponse.self,
from: data) else { return }

print("Price returned: \(response.data.amount)")

// 5. Update the UI with the parsed PriceResponse 更新UI信息
DispatchQueue.main.async { [weak self] in
self?.updateLabel(price: response.data)
}
}

task.resume()
}

解析代码:

  1. 创建请求Bitcoin价格的URLRequest

  2. 创建URLSessionDataTask网络请求Bitcoin价格任务。如果HTTP请求成功,会返回一段JSON格式的数据:

    {
    "data": {
    "base": "BTC",
    "currency": "USD",
    "amount": "15840.01"
    }
    }
  3. 每个HTTP请求后都应该检测错误,这里也不例外。

  4. 使用JSONDecoder解析JSON数据,并map到modle对象PriceResponse

  5. 异步传输model数据及更新UI。

格式化

BitcoinViewController 中的updateLabel(price:)函数任务是更新Label内容,确保正确显示比特币价格,包括整数的美元及小数的美分。

总结下,一个UIViewController内包含了不同的业务逻辑,如网络请求数据解析格式化,而且他们紧紧的耦合在一起。很难独立于整个BitcoinViewController对象测试它的任何部分,也很难在其他地方重用相同的逻辑。

That doesn’t sound good – can we fix this?

对于紧密耦合的另外一面就是建立松散耦合对象,可以轻松链接,轻松解除。

是时候重构BitcoinViewController了,以便它为网络请求和数据解析职责创建单独的对象。完成之后,您将使用Swinject调整它们的使用,以实现真正的解耦组件。

剥离依赖关系

首先创建一个名为Dependencies的新文件夹。这将保存您将在本教程其余部分中提取的所有逻辑块。右键单击Bitcoin Adventurer文件夹并选择New Group。然后将其名称设置为Dependencies。

下面开始分离业务逻辑之旅吧!让代码更利于测试,更健壮,更漂亮。

剥离网络层逻辑

在Dependencies文件夹下创建一个文件:HTTPNetworking.swift。添加以下的代码,根据注释理解代码含义。

// 1.定义Networking协议,包含一个方法request(from:completion:),返回Data或者Error
protocol Networking {
typealias CompletionHandler = (Data?, Swift.Error?) -> Void

func request(from: Endpoint, completion: @escaping CompletionHandler)
}

// 2.创建网络层HTTP协议的实现
struct HTTPNetworking: Networking {
// 3.协议方法实现,创建网络请求,根据指定的网址
func request(from: Endpoint, completion: @escaping CompletionHandler) {
guard let url = URL(string: from.path) else { return }
let request = createRequest(from: url)
let task = createDataTask(from: request, completion: completion)
task.resume()
}

// 4.创建URLRequst的子方法
private func createRequest(from url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringCacheData
return request
}

// 5.发送网络请求的子方法
private func createDataTask(from request: URLRequest,
completion: @escaping CompletionHandler) -> URLSessionDataTask {
return URLSession.shared.dataTask(with: request) { data, httpResponse, error in
completion(data, error)
}
}
}

好了,开始使用网络逻辑层代码,打开BitcoinViewController,并在顶端三个IBOutles下面添加如下代码:

let networking = HTTPNetworking()

可以修改requestPrice()代码了,如下:

networking.request(from: Coinbase.bitcoin) { data, error in
// 1. Check for errors 检测网络错误
if let error = error {
print("Error received requesting Bitcoin price: \(error.localizedDescription)")
return
}

// 2. Parse the returned information 解析网络返回的JSON数据
let decoder = JSONDecoder()
guard let data = data,
let response = try? decoder.decode(PriceResponse.self, from: data)
else { return }

print("Price returned: \(response.data.amount)")

// 3. Update the UI with the parsed PriceResponse 更新UI
DispatchQueue.main.async { [weak self] in
self?.updateLabel(price: response.data)
}
}

再次运行项目,依然可以正常工作。漂亮!你已经成功的剥离出网络层业务逻辑。然而为了最终的依赖注入,还有更多的解耦合要做。

剥离数据解析层逻辑

同样在Dependencies目录下建立一个文件:BitcoinPriceFetcher.swift。写入如下代码:

protocol PriceFetcher {
func fetch(response: @escaping (PriceResponse?) -> Void)
}

struct BitcoinPriceFetcher: PriceFetcher {
let networking: Networking

// 1. Initialize the fetcher with a networking object
init(networking: Networking) {
self.networking = networking
}

// 2. Fetch data, returning a PriceResponse object if successful
func fetch(response: @escaping (PriceResponse?) -> Void) {
networking.request(from: Coinbase.bitcoin) { data, error in
// Log errors if we receive any, and abort.
if let error = error {
print("Error received requesting Bitcoin price: \(error.localizedDescription)")
response(nil)
}

// Parse data into a model object.
let decoded = self.decodeJSON(type: PriceResponse.self, from: data)
if let decoded = decoded {
print("Price returned: \(decoded.data.amount)")
}
response(decoded)
}
}

// 3. Decode JSON into an object of type 'T'
private func decodeJSON<T: Decodable>(type: T.Type, from: Data?) -> T? {
let decoder = JSONDecoder()
guard let data = from,
let response = try? decoder.decode(type.self, from: data) else { return nil }

return response
}
}

注意:PriceFetcher协议定义了一个方法: 一个执行获取并返回PriceResponse对象的方法。这个“fetch”可以从任何数据源发起请求,而不一定是HTTP请求。当您开始编写单元测试时,需要Mock一些本地数据,这将成为该协议的一个重要特征。这里fetch发起的请求使用的是新创建的网络协议。

现在有个一个更具体的抽象层逻辑来获取比特币价格,是时候再出重构BitcoinViewController来使用它了。

替换代码:

let networking = HTTPNetworking()

为:

let fetcher = BitcoinPriceFetcher(networking: HTTPNetworking())

然后再出修改requestPrice()的实现代码:

private func requestPrice() {
fetcher.fetch { response in
guard let response = response else { return }

DispatchQueue.main.async { [weak self] in
self?.updateLabel(price: response.data)
}
}
}

现在上面的代码看上去更简洁和易读,因为把繁重的任务交给依赖项BitcoinPriceFetcher去处理了。

再次运行项目,依然可以正常工作,恭喜你,你通过使用依赖注入技术(DI)提高了代码质量。

那么,下一篇我们一起看看单元测试。

~ END ~

~ END ~