Swift + RxSwift MVVM 模块化项目实践

本文主要介绍个人在 Swift 项目开发中的一些实践经验,供大家所借鉴或者探讨。

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

项目介绍

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

Demo 效果演示

App 架构设计

最顶层为 主工程,包含一些简单的配置、路由注册等,相当于一个空壳,模块化之后需要注意的一点是:模块的版本管理,每次发版一定要记录好每个模块的版本号等,否则代码回退、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 简单修改完成的。

上下文模块(Bella)

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

项目配置模块(Alice)

包括项目的主题等各个模块的配置,涉及所有业务模块的主题颜色配置,以及一些第三方库的 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 结合使用,使我们的开发效率大大提高了,节省出来的时间我们专注于业务功能的开发。

🤝 最后感谢您的阅读!

原文地址:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 本文主要介绍个人在 Swift 项目开发中的一些实践经验,供大家所借鉴或者探讨。 提高开发效率,降低 Bug 发生...
    seongbrave阅读 1,560评论 1 9
  • 路由是实现模块间解耦的一个有效工具。如果要进行组件化开发,路由是必不可少的一部分。目前iOS上绝大部分的路由工具都...
    黑超熊猫zuik阅读 3,917评论 8 52
  • 文/逐梦水乡 我们这里,自古就有四月八涨大水的说法。看来气象专家的预测是对的,极端天气的说法得到了印证。 人们都说...
    逐梦水乡阅读 1,219评论 32 45
  • 陌生女:韩一成! 韩一成:额,你在跟踪我吗,你想干嘛? 陌生女:不是的,我这不算跟踪你。 韩一成:那到底为什么跟着...
    Xfor阿妤阅读 353评论 3 5
  • 立秋了。 我最爱的季节 冯唐有句诗:秋天短到没有,你我短到回不到过去。幸而,在秋天,有你与我一同赏味人生;在秋天,...
    月羊不是羊阅读 131评论 0 0