Swift + RxSwift MVVM 模块化项目实践

本文主要介绍我的在 Swift 项目开发中的一些实践经验,供你们所借鉴或者探讨。html

提升开发效率,下降 Bug 发生率,是咱们每一个开发所追随的目标。我的认为经过 CocoaPods 实现模块化组件化,积累适合的组件模块,重复利用公用模块,不只能够提升开发效率而且能够有效的下降 Bug 的发生,另外能够借助 Gckit-CLI 等脚本工具下降重复无用的代码编写,进一步提升开发效率,下降低级错误的发生,本文如下内容主要讲解我的经过 CocoaPods 结合 Gckit-CLI 实现开发效率的最大化的一些项目实践react

项目介绍

Twilight,项目取自暮光之城电影名 全部的资源都已经开源到 Github 上了,包括服务端的接口项目git

Demo 效果演示github

App 架构设计

structurechart.png

最顶层为 主工程,包含一些简单的配置、路由注册等,至关于一个空壳,模块化以后须要注意的一点是:模块的版本管理,每次发版必定要记录好每一个模块的版本号等,不然代码回退、Bug 排查是一件很困难的事,咱们主工程中会记录每次发版时各个模块的版本号的。接下来就是业务层,包括各个不一样的业务模块,这些模块之间的调用是经过路由实现的,不能存在引用关系的,每一个模块会依赖一个上下文模块项目配置模块上下文模块主要是管理用户对象等用户权限相关的事,项目配置模块主要是总体 App 的一些配置数据、以及主题颜色和一些第三方 key 的配置等(主要为了方便配置统一管理)。业务层是整个 App 的核心功能,而公用组件模块是跨业务、跨 App 的,不一样的 App 之间是能够公用这些组件的,这一层最好做为公司级别的供你们全部人使用。最下层为第三方库,通常状况下咱们须要对第三方作一层脱离耦合的封装,以便咱们在修改第三方时而不影响咱们的业务模块。整个项目从上到下为依赖关系,下层为上层提供功能服务。正则表达式

业务模块

模块 介绍 地址
Carlisle 登录注册模块 https://github.com/SeongBrave/Carlisle.git
Bella 上下文模块 https://github.com/SeongBrave/Bella.git
Alice 项目配置模块 https://github.com/SeongBrave/Alice.git
Jacob 首页模块 https://github.com/SeongBrave/Jacob
Twilight 主工程项目 https://github.com/SeongBrave/Twilight.git
TwilightSpecs CocoaPods 私有仓储 https://github.com/SeongBrave/TwilightSpecs

登录注册模块(Carlisle)编程

包含用户注册、登录、找回密码等功能,主要是用户权限相关的管理界面,登录注册模块是参考RxSwift官方 Demo 简单修改完成的。swift

上下文模块(Bella)api

上下文模块主要用于用户对象的管理,后期会把考虑把本地缓存等加密功能加上,上下文模块被每一个业务模块所依赖,用于管理用户上下文对象,同步用户信息的修改。缓存

项目配置模块(Alice)bash

包括项目的主题等各个模块的配置,涉及全部业务模块的主题颜色配置,以及一些第三方库的 key,各个模块的通知等。

首页模块(Jacob)

商品列表模块 取值暮光之城中 -Jacob

该模块 90% 的代码是经过Gckit-CLI生成的,一键生成包含了大部分的逻辑代码, 上拉加载更多、下拉刷新、错误提示、出错重试处理等逻辑,这些大部分的逻辑代码是不须要修改的。

目录结构:

├── Api
│   ├── Home_api.swift
│   └── Product_api.swift
├── Model
│   ├── Home_model.swift
│   └── Product_model.swift
├── Module
│   ├── JacobCore.swift
│   └── Jacob_router.swift
├── View
│   └── tCell
│       ├── Home_tCell.swift
│       └── Product_tCell.swift
├── ViewController
│   ├── Home_vc.swift
│   └── Product_vc.swift
└── ViewModel
    ├── Home_vm.swift
    └── Product_vm.swift
复制代码

目录结构分为:

  • Api: 接口 Api
  • Model: 实例 Model
  • Module: 模块相关管理类,包含路由注册和提供别的模块访问的管理类
  • View: 相关自定义的 View
  • ViewController: 对应的 ViewController
  • ViewModel: 对应的 ViewModel
/// 界面第一次初始化
 let _ =  Observable.of(
     input.firstLoadTriger,
     reloadTrigger.withLatestFrom(input.firstLoadTriger))
     .merge().map{ Home_api.homes(page: 0, pageSize: 10)}.share(replay: 1)
     .emeRequestApiForArray(Home_model.self,activityIndicator: loading)
     .subscribe(onNext: {[unowned self] (result) in
         switch result {
         case .success(let data):
             self.hasNextPage.value = data.count == 10
             self.homeElements.value = data
             self.page = 1
         case .failure(let error):
             self.refresherror.onNext(error)
         }
     })
     .disposed(by: disposeBag)
复制代码

上面的代码 经过信号筛选,reloadTrigger表明点击从新加载的事件,通过参数格式化、发送网络请求、数据解析等数据处理,最后只需关注解析成功以后的 Model 数据而后更新 UI 界面。

公用模块

公司的公用组件应该是长期积累的,不一样的该功能,大部分是与业务无关的能够扩 App 或者夸业务使用的,通过长时间的积累会慢慢完善,好比京东内部有各类各样的模块组件,对与新开发一个项目来讲会提升不少倍,这些公用组件模块经过 CocoaPods 管理,或者也能够经过 Framework 管理

如下是我我的积累的一些公用库,日常写 Demo 啥的都是很是方便的

模块 介绍 地址
UtilCore 基础工具库 https://github.com/SeongBrave/UtilCore
NetWorkCore 网络工具库 https://github.com/SeongBrave/NetWorkCore
EmptyDataView 列表为空时自定义展现空界面 https://github.com/SeongBrave/EmptyDataView

RxSwift 的使用

项目中大部分的逻辑处理是借助 RxSwift 实现的响应式编程,当界面上的每一个操做都会转换为一个信号而后经过对信号的各类加工网络请求,到返回的数据 JSON 解析以及错误对象的处理,感受整个开发都是在开凿水渠,等开发完了就不用管了。

网络请求

NetWorkCore经过对Alamofire简单封装,配合RxSwift能够很简单的实现一个网络请求,而且完成数据解析对应的 Mode 实体类,以下所示,便可实现一个用户登陆的网络请求。

input.loginTaps
            .withLatestFrom(Observable.combineLatest(input.username, input.password) { ($0, $1) })
            .map{Carlisle_api.login(phone: $0, password: $1)}
            .emeRequestApiForObj(User_Model.self, activityIndicator: loading)
            .subscribe(onNext: {[unowned self] (result) in
                switch result {
                case .success(let user):
                    //登录成功就更新上下文中的登录对象
                    Global.updateUserModel(user)
                    self.loginSuccess.onNext(user)
                case .failure(let error):
                    self.error.onNext(error)
                }
            })
            .disposed(by: disposeBag)
复制代码

模块路由

Swift 下一直使用URLNavigator做为模块之间的路由框架使用,感受很是方便

extension String {
    /// 返回路由路径
    ///
    /// - Parameter param: 请求参数
    public func getUrlStr(param:[String:String]? = nil) -> String {
        let that = self.removingPercentEncoding ?? self
        let appScheme = Navigator.scheme
        let relUrl = "\(appScheme)://\(that)"
        guard param != nil else {
            return relUrl
        }
        var paramArr:[String] = []
        for (key , value) in param!{
            paramArr.append("\(key)=\(value)")
        }
        let rel = paramArr.joined(separator: "&")
        guard rel.count > 0 else {
            return  relUrl
        }
        return relUrl + "?\(rel)"
    }
    /// 直接经过路径 和参数调整到 界面
    public func openURL( _ param:[String:String]? = nil) -> Bool {
        let that = self.removingPercentEncoding ?? self
        /// 为了使html的文件通用 须要判断是否以http或者https开头
        guard that.hasPrefix("http") || that.hasPrefix("https") || that.hasPrefix("\(Navigator.scheme )://") else {
            var url = ""
            ///若是以 '/'开头则须要加上本服务域名
            if that.hasPrefix("/") {
                url = UtilCore.sharedInstance.baseUrl + that
            }else{
                url = that.getUrlStr(param: param)
            }
            // 首先须要判断跳转的目标是不是界面仍是处理事件 若是是界面须要: push 若是是事件则须要用:open
            let isPushed = Navigator.that?.push(url) != nil
            if isPushed {
                return true
            } else {
                return (Navigator.that?.open(url)) ?? false
            }
        }
        // 首先须要判断跳转的目标是不是界面仍是处理事件 若是是界面须要: push 若是是事件则须要用:open
        let isPushed = Navigator.that?.push(that) != nil
        if isPushed {
            return true
        } else {
            return (Navigator.that?.open(that)) ?? false
        }
    }
}
复制代码

这块其实能够更进一步的封装,好比每次调整均可以经过正则表达式进行有效性的验证,或者一些其余路由规则判断

借助URLNavigator实现各个模块的解耦,理论上每一个界面均可以实现互相跳转的,在处理商品列表界面的行点击事件(didSelectRowAt)的时候是由服务端返回的uri字段决定的,具体跳转哪一个界面是有服务端决定的,我的的理解是界面负责产生信号,每一个信号都会通过复杂的筛选变化又会反应到界面上的,全部的跳转事件均可以经过 URLNavigator 路由实现,好比逻辑处理、界面跳转等事件

每一个模块都有各自的模块路由注册类,好比Jacob_router.swift,包含了该模块内部全部的可路由的界面和事件处理的路由注册,最后会在主模块中统一注册

错误处理

监控整个 App 的全部错误,而后经过一些规则筛选最后展现给用户是咱们在开发一个 App 的时候须要考虑处理的,好比在下拉列表的时候,发送网络请求,这时候网络请求失败了,须要界面上展现网络错误,而且显示从新加载的按钮,或者是若是在调用相机获取受权的时用户没有受权的时候,须要提示给用户受权相关的信息,等等这些逻辑处理均可以经过流的形式处理,在处理用户网络错误加载失败的时候,经过 RxSwift 的一个很简单的 Api:withLatestFrom就能实现数据从新加载,而不须要记住各类复杂的参数。

根据错误码的不一样进行不一样的错误逻辑处理,以下代码所示

/** 经过 mikerError 显示错误信息 202024: 请登陆后再操做 - parameter error: */
    public func toastError(_ error:MikerError){
        if error.code == UtilCore.sharedInstance.toLoginErrorCode {
            self.toastCompletion(error.message){ _ in
                /** * 在这块 就是跳转到登录模块,若是已经跳转就不须要直接忽略 不然 先将AppData.sharedInstance.isHasToLoginVc改成true而后再跳转 */
                if UtilCore.sharedInstance.isHasToLoginVc == false {
                    _ = "login".openURL()
                }
            }
        } else if error.code == UtilCore.sharedInstance.toForcedupdatingErrorCode {
            /* 表示版本强制更新 */
            if UtilCore.sharedInstance.isHasForcedupdating == false {
                UtilCore.sharedInstance.isHasForcedupdating = true
                _ = "forcedupdating".openURL(["message":error.message])
            }

        } else {
            if UtilCore.sharedInstance.isDebug {
                self.toast(error.message)
            } else {
                 ///表示是生产模式
                let code = "\(error.code)"
                if code.hasPrefix("2") {
                    self.toast(error.message)
                } else {
                    self.toast(UtilCore.sharedInstance.errorMsg)
                }
            }
        }
    }
复制代码

指令码

与服务端确认配合肯定,经过错误码路由结合能达到一种指令码的效果,客户端取到服务端返回的错误码的时候先进行逻辑判断,适配一些规则,若是符合则取服务端返回的uri字段,直接进行路由跳转,不然走错误处理抛出。这种指令码能够达到一些客户端的跳转逻辑交由服务端来控制,好比在注册完毕以后是跳转首页仍是继续补充完详细信息的这种需求是能够根据服务端返回的指令码来决定。

MVVM 架构设计

一直以为南峰子翻译的这两篇文章挺不错的虽然是 2014 的文章了,感兴趣的能够看下

另外登录注册模块(Carlisle)是参考RxSwift官方 Demo 设计的,使用 MVVM 架构设计,虽然没有严格遵照上面文章所说的 MVVM 引用层次,不过登录注册模块(Carlisle)仍是能够灵活的适用于不一样的需求的在简单修改以后。

Gckit-CLI 的使用

CocoaPods 公共组件模块能够很方便集成现有的模块,可是咱们每一个业务都是彻底不同的,每一个接口返回的 JSON 文件也不同,而后咱们得手动建立与之对应的 Model,这些操做彻底没有任何意义可是又是必须的,不过如今咱们可使用 Gckit-CLI 一键生成对应的全部 Model 实体类,咱们只须要把对应的 JSON 文件放到对应的目录便可,Gckit-CLI 不只能够生成 Model 文件,ViewModel、ViewController、View、Cell 等各类文件,而且是一键生成,你们能够尝试使用下,若是以为能够的话麻烦给一个Star吧 😂。

Node.js 接口服务

twilight_app 为项目后台的接口服务,一个客户端开发的思惟开发的后台接口服务 😂,功能很简单,若是感兴趣的能够下载看下

总结

本文简单介绍了本身在 Swift 模块化项目中的一些实践经验,借助 RxSwift 实现 MVVM 框架的设计,内容比较杂,供你们参考,随着 Swift 5 的发布,Swift ABI 的稳定,相信会有更多团队会选择 Swift 语言开发本身的 App 的, 周围认识的不少朋友都说若是尝试过 Swift 以后就很难再回去用 Objective-C 了,Swift 自己带有的不少特性是 Objective-C 不具备的,呀感受又扯远了,我我的比较喜欢经过一些工具去实现一些效率方面的提高的,经过模块化实现代码的复用,经过一些脚本工具实现重复无用代码的自动生成,好比 Model 文件的生成等,这样咱们经过借助 CocoaPods 和 Gckit-CLI 结合使用,使咱们的开发效率大大提升了,节省出来的时间咱们专一于业务功能的开发。

🤝 最后感谢您的阅读!

相关文章
相关标签/搜索