用 Swfit 搭建一个完整项目
Swift 目前更新到了 Swift4,已经相当稳定,相比于之前的版本跳跃就得重学的情况,从 Swift3 更新到 Swift4 的成本非常小。
再加上苹果极力推行 Swift,可以预见在不远的未来,Objective-C 将会被淘汰,现在可见最多的就是混编,OC 项目添加 Swift 代码,将老项目一步步更改为 Swift 版本,这样也是学习和研究 Swift 的一种方法。但这种方式,我们的基础代码和架构依然是 OC,那么怎样使用 Swift 搭建一个项目并完全使用 Swift 就是一个需要解决的问题。
这里要讨论的,就是如何用 Swift 搭建一个项目。首先表明这里只是针对单个项目,对于组件化等开发模式来说,会更加复杂,但是基础还是可以通用的。
对于一个 iOS 项目,在搭建项目的时候,使用的架构为 MVVM 架构,需要考虑的有以下几点:
- 数据模型
- 网络请求
- 缓存
- 代码架构 - MVVM
- 图片加载
- 工具类
- 单元测试
- 安全
在考虑的时候,优先根据 MVVM 考虑各个模块的分工,这里把 MVVM 放到了第四位,只是因为在实现 viewModel 和 view 的时候,需要前面的支持。
RxSwift
在介绍之前,先说一下 RxSwift,ReactiveCocoa 这个之前应该很多人使用,但凡说是要做 MVVM 架构,都会加入 ReactiveCocoa,而 Swift 版的就叫做 RxSwift。想要深入学习的,可以搜索一下,相关文章很多,这里就不做解释。
在本次的项目中,RxSwift 会非常频繁的被使用,在以上所列出来的模块中,基本每个模块都有涉及到。
1. 数据模型
数据是程序运行的基础,也对应了 MVVM 架构中的 Model ,它负责整个数据的构建及内部数据处理。
从使用数量上来说,Json 依然是数据传输的普遍格式,protobuf 是 Google 推出的一个数据交换格式,它更小,更迅速,也更加安全,这在未来是趋势,但现在还是 Json 更普遍。
在这里,使用的是 HandyJSON 来进行数据解析,其优势是可以自动进行解析,代码简单,还会进行自动类型转换,这样即使约定好的数据类型发生变化,也不会影响到程序的正常运行。
一般服务器传来的数据都会进行格式化,最外层的结构都是一样的,例如:
{
"code": 200,
"msg": "success",
"datas": {
// 对应的数据
}
}
所有的数据返回,都会是以这样的结构传递回来,不同的是 datas 的内容。按照这样的思路,我们可以做一个基类叫做 Base,它的责任就是负责解析这些通用的数据。对于这个 code,200 是成功,其他的值就是失败,详细点的话,会有各种不同的对应,就像 http 的状态码一样,因此这个 code 可以做成一个枚举。代码如下:
/// 服务器规定,接口调用成功为 200,其他数字则为失败
///
/// - success: 请求成功
/// - fail: 未定义的请求失败
/// - logout: 强制登出
enum ResponseCode: Int, HandyJSONEnum {
case success = 200
case fail = -1
case logout = 10504
}
/// response 最外层的数据,内层模型不需要继承 base
class Base: HandyJSON {
// 未定义的 code 有千万种,因此默认请求的结果是未定义错误
var code: ResponseCode = .fail
var message: String = "Resquest something error"
required init() {}
func mapping(mapper: HelpingMapper) {
mapper <<<
[
self.code <-- "code",
self.message <-- "msg",
]
}
}
这样,我们一个基础的模型就做好了,后面所有符合这个规则的模型,都会继承这个类,然后实现自己的 datas 转意。例如一个用户返回类:
// 返回里有 Base 中的字段:code、msg,因此 response 继承自 Base
class UserResponse: Base {
var user: User?
override func mapping(mapper: HelpingMapper) {
super.mapping(mapper: mapper)
mapper <<<
self.user <-- "prData"
}
}
// 而作为我们真正使用的 User 类,它是没有 Base 的数据的,所以不用继承
class User: HandyJSON {
var email: String?
var userName: String?
var userToken: String?
required init() { }
func mapping(mapper: HelpingMapper) {
mapper <<<
[
self.email <-- "mail",
self.userName <-- "userName",
self.userToken <-- "utoken"
]
}
}
在做数据模型的时候,一定不要在其他地方做任何解析 Json 的行为,见过有很多人在 ViewModel 里面请求到数据后进行数据解析,将 code 那一层使用 NSDictionary 来通过键值来取,判定完状态后,再将 datas 里面的内容解析成数据模型。这样的问题就是会有很多冗余代码,在写代码的时候会非常痛苦,后期维护也会非常困难。最重要的是,这完全不符合 MVVM 或 MVC 的设计模式。
在 Model 的内部数据处理中,我们可以将一些转换操作和对应类的某些操作放在模型中,比如我们要给用户添加一个创建日期的字段,服务端传来的数据一般是时间戳:
"craete_date": 4383920100
写转换的时候,可以将时间戳转换为我们需要的 Date 类型这一过程放到 Model 中。
var create_date: Int64?
var createDate: Date? {
get {
guard let create_date = create_date else { return nil }
// 将 create_date 转换为 Date
let resultDate = Date(timeIntervalSince1970: create_date)
return resultDate
}
}
2. 网络请求
网络请求是项目中非常重要的模块,在这里我们使用 Alamofire 进行网络请求,一般将这个类命名为 API。在这里,我们使用了 Alamofire + RxSwift + HandyJSON 组合的方式,将数据请求到数据解析这一过程整合在一起。
2.1 错误类型
在正式介绍之前,先说一下错误类型,在 Swift 中是没有错误类的,有一个 Error 的协议,我们所有的错误要遵循这个协议来进行开发,当然 NSError 还是可以使用的,但非必须的时候不建议使用 OC 的类。我们先定义一个 APIError 的类,它囊括了在 API 请求的过程中可能出现的所有错误类型:
enum APIError: Error {
/*
请求出错,服务器会返回对应的描述文字
*/
case error(wrong: String)
/*
服务器返回的数据不是 JSON
*/
case dataJSON(wrong: String)
/*
服务器返回的数据为空
*/
case dataEmpty(wrong: String)
/*
服务器返回的数据不能解析
*/
case datamatch(wrong: String)
/*
网络请求错误
*/
case network(wrong: String)
/*
使用 NSError 创建网络请求错误
*/
static func networkWrong(with error: NSError) -> APIError {
if let errorMessage = error.userInfo["NSLocalizedDescription"] as? String {
return APIError.network(wrong: errorMessage)
}
if error.domain == "Alamofire.AFError" {
// 处理 Alamofire 返回的错误
if error.code == 4 {
return APIError.dataEmpty(wrong: "Server return data is nil or zero length.")
}
}
return APIError.network(wrong: "Unknown Network Wrong.")
}
}
// 这里写了一个 Extension 用来做错误的提示
// 注意这里会有重复代码,但是为了给错误做区分,还是使用了这种方式
// 这里可以考虑将 APIError 写成一个 Class,然后添加一个 ErrorType 的枚举,可能代码会少一点,但是在使用的时候其实还是一样的
// 当然,如果不考虑错误类型,那就不需要 ErrorType ,直接一个错误描述就可以。
extension APIError {
func showHUD() {
switch self {
case .error(let wrong):
HUD.showError(with: wrong)
break
case .dataEmpty(let wrong):
HUD.showError(with: wrong)
break
case .dataJSON(let wrong):
HUD.showError(with: wrong)
break
case .datamatch(let wrong):
HUD.showError(with: wrong)
break
case .network(let wrong):
HUD.showError(with: wrong)
break
}
}
}
2.2 路径
每个请求都会有自己的路径,针对路径我们需要做一个类来专门进行维护,Alamofire 推荐的方法是写成一个 enum 并且遵循其 URLConvertible 或 URLRequestConvertible 协议。在这里,我们使用了 URLConvertible 协议。
代码如下:
/// 请求路由
enum Router: String, URLConvertible {
/// 登录
case login = "Account/login"
/// 注册
case register = "Account/register"
/// 其他
/// 域名
static var baseURL: String {
get {
if Cache.isTestEnvironment {
// 测试环境
return "http://api.beta.xxxxx.com/Api/"
} else {
// 正式环境
return "https://api.xxxxx.com/Api/"
}
}
}
var appendURL: String {
get {
return rawValue
}
}
func asURL() throws -> URL {
let nowTimeInterval = Int64(NSDate().timeIntervalSince1970)
let urlString = Router.baseURL.appending(appendURL) + "?t=\(nowTimeInterval)"
return URL(string: urlString)!
}
}
2.3 请求
对于 API 类,我们需要一个基础的方法来做所有的请求,并处理相关数据。首先我们需要设定请求头,一般情况下,我们所有的请求头内容都是固定的。经过讨论和 http2.0 的发展趋势来看,header 内部尽量减少内容,不要添加太多。以下为配置代码:
class API {
static var headers: HTTPHeaders {
get {
let os = "iOS"
let client = UIDevice.sysNameVersion // 获取系统的版本号,在工具类中
let result: [String: String] = ["os": os, "client": client]
return result
}
}
}
在具体实现中,为了在这一步就可以完成数据解析,我们使用一个 <T: HandyJSON>
和 returnType: T.type
来确定返回的数据类型,<T: HandyJSON>
决定了我们传过来的 returnType
必须是遵循 HandyJSON
协议的类型,然后使用 returnType
进行数据解析,就可以得到对应类型的数据,具体实现如下:
extension API {
/// 将 Alamofire、RxSwift、HandyJSON 结合
///
/// 可以通过 RxSwift 的方式通过 Alamofire 获取到数据后使用 HandyJSON 将数据转换成需要的模型
///
/// - Parameters:
/// - url: 访问路径
/// - method: 请求方式
/// - uploadData: 请求参数
/// - parameters: 上传数据
/// - returnType: 返回模型的类型
/// - Returns: Observable<HandyJSON> onNext 说明请求正确,发送的数据就是需要的数据,onError 说明请求失败,会返回失败原因
fileprivate class func request<T: HandyJSON>(_ url: Router, method: HTTPMethod = .post, uploadData: Data? = nil, parameters: Parameters?, returnType: T.Type) -> Observable<T> {
return Observable.create({ observer in
if let uploadData = uploadData {
upload(observer: observer, url: url, method: method, uploadData: uploadData, parameters: parameters, returnType: returnType)
} else {
requestData(observer: observer, url: url, method: method, parameters: parameters, returnType: returnType)
}
return Disposables.create {}
})
}
/// Post 请求
///
/// - Parameters:
/// - observer: Rx 观察者, 用来向外部传递数据
/// - url: 请求链接
/// - method: 请求方式
/// - parameters: 请求参数
/// - returnType: 返回值类型
fileprivate class func requestData<T: HandyJSON>(observer: AnyObserver<T>, url: Router, method: HTTPMethod = .post, parameters: Parameters?, returnType: T.Type) {
Alamofire.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON(completionHandler: { response in
switch response.result {
case .success:
self.successHandle(observer: observer, result: response.result, returnType: returnType)
break