用 Swfit 搭建一个完整项目

用 Swfit 搭建一个完整项目

Swift 目前更新到了 Swift4,已经相当稳定,相比于之前的版本跳跃就得重学的情况,从 Swift3 更新到 Swift4 的成本非常小。

再加上苹果极力推行 Swift,可以预见在不远的未来,Objective-C 将会被淘汰,现在可见最多的就是混编,OC 项目添加 Swift 代码,将老项目一步步更改为 Swift 版本,这样也是学习和研究 Swift 的一种方法。但这种方式,我们的基础代码和架构依然是 OC,那么怎样使用 Swift 搭建一个项目并完全使用 Swift 就是一个需要解决的问题。

这里要讨论的,就是如何用 Swift 搭建一个项目。首先表明这里只是针对单个项目,对于组件化等开发模式来说,会更加复杂,但是基础还是可以通用的。

对于一个 iOS 项目,在搭建项目的时候,使用的架构为 MVVM 架构,需要考虑的有以下几点:

  1. 数据模型
  2. 网络请求
  3. 缓存
  4. 代码架构 - MVVM
  5. 图片加载
  6. 工具类
  7. 单元测试
  8. 安全

在考虑的时候,优先根据 MVVM 考虑各个模块的分工,这里把 MVVM 放到了第四位,只是因为在实现 viewModel 和 view 的时候,需要前面的支持。

RxSwift

在介绍之前,先说一下 RxSwiftReactiveCocoa 这个之前应该很多人使用,但凡说是要做 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
      
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值