Swift + RxSwift MVVM 模塊化項目實踐

banner.png

本文主要介紹個人在 Swift 項目開發(fā)中的一些實踐經(jīng)驗导街,供大家所借鑒或者探討耻蛇。

提高開發(fā)效率吭从,降低 Bug 發(fā)生率朝蜘,是我們每個開發(fā)所追隨的目標。個人認為通過 CocoaPods 實現(xiàn)模塊化組件化涩金,積累適合的組件模塊芹务,重復利用公用模塊,不僅可以提高開發(fā)效率并且可以有效的降低 Bug 的發(fā)生鸭廷,另外可以借助 Gckit-CLI 等腳本工具降低重復無用的代碼編寫,進一步提高開發(fā)效率熔吗,降低低級錯誤的發(fā)生辆床,本文以下內(nèi)容主要講解個人通過 CocoaPods 結合 Gckit-CLI 實現(xiàn)開發(fā)效率的最大化的一些項目實踐

項目介紹

Twilight,項目取自暮光之城電影名
所有的資源都已經(jīng)開源到 Github 上了桅狠,包括服務端的接口項目

Demo 效果演示

showapp.gif

App 架構設計

structurechart.png

最頂層為 主工程讼载,包含一些簡單的配置、路由注冊等中跌,相當于一個空殼咨堤,模塊化之后需要注意的一點是:模塊的版本管理,每次發(fā)版一定要記錄好每個模塊的版本號等漩符,否則代碼回退一喘、Bug 排查是一件很困難的事,我們主工程中會記錄每次發(fā)版時各個模塊的版本號的。接下來就是業(yè)務層凸克,包括各個不同的業(yè)務模塊议蟆,這些模塊之間的調(diào)用是通過路由實現(xiàn)的,不能存在引用關系的萎战,每個模塊會依賴一個上下文模塊項目配置模塊咐容,上下文模塊主要是管理用戶對象等用戶權限相關的事,項目配置模塊主要是整體 App 的一些配置數(shù)據(jù)蚂维、以及主題顏色和一些第三方 key 的配置等(主要為了方便配置統(tǒng)一管理)戳粒。業(yè)務層是整個 App 的核心功能,而公用組件模塊是跨業(yè)務虫啥、跨 App 的蔚约,不同的 App 之間是可以公用這些組件的,這一層最好作為公司級別的供大家所有人使用孝鹊。最下層為第三方庫炊琉,一般情況下我們需要對第三方做一層脫離耦合的封裝,以便我們在修改第三方時而不影響我們的業(yè)務模塊又活。整個項目從上到下為依賴關系苔咪,下層為上層提供功能服務。

業(yè)務模塊

模塊 介紹 地址
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)

上下文模塊主要用于用戶對象的管理,后期會把考慮把本地緩存等加密功能加上曲初,上下文模塊被每個業(yè)務模塊所依賴体谒,用于管理用戶上下文對象,同步用戶信息的修改臼婆。

項目配置模塊(Alice)

包括項目的主題等各個模塊的配置抒痒,涉及所有業(yè)務模塊的主題顏色配置,以及一些第三方庫的 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代表點擊重新加載的事件寂汇,經(jīng)過參數(shù)格式化、發(fā)送網(wǎng)絡請求捣染、數(shù)據(jù)解析等數(shù)據(jù)處理骄瓣,最后只需關注解析成功之后的 Model 數(shù)據(jù)然后更新 UI 界面。

公用模塊

公司的公用組件應該是長期積累的耍攘,不同的該功能榕栏,大部分是與業(yè)務無關的可以擴 App 或者夸業(yè)務使用的,經(jīng)過長時間的積累會慢慢完善蕾各,比如京東內(nèi)部有各種各樣的模塊組件扒磁,對與新開發(fā)一個項目來說會提高很多倍,這些公用組件模塊通過 CocoaPods 管理式曲,或者也可以通過 Framework 管理

以下是我個人積累的一些公用庫妨托,平常寫 Demo 啥的都是非常方便的

模塊 介紹 地址
UtilCore 基礎工具庫 https://github.com/SeongBrave/UtilCore
NetWorkCore 網(wǎng)絡工具庫 https://github.com/SeongBrave/NetWorkCore
EmptyDataView 列表為空時自定義展示空界面 https://github.com/SeongBrave/EmptyDataView

RxSwift 的使用

項目中大部分的邏輯處理是借助 RxSwift 實現(xiàn)的響應式編程,當界面上的每個操作都會轉(zhuǎn)換為一個信號然后通過對信號的各種加工網(wǎng)絡請求吝羞,到返回的數(shù)據(jù) JSON 解析以及錯誤對象的處理兰伤,感覺整個開發(fā)都是在開鑿水渠,等開發(fā)完了就不用管了钧排。

網(wǎng)絡請求

NetWorkCore通過對Alamofire簡單封裝敦腔,配合RxSwift可以很簡單的實現(xiàn)一個網(wǎng)絡請求,并且完成數(shù)據(jù)解析對應的 Mode 實體類恨溜,如下所示符衔,即可實現(xiàn)一個用戶登錄的網(wǎng)絡請求。

 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: 請求參數(shù)
    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)"
    }
    /// 直接通過路徑 和參數(shù)調(diào)整到 界面
    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)
            }
            // 首先需要判斷跳轉(zhuǎn)的目標是否是界面還是處理事件 如果是界面需要: push 如果是事件則需要用:open
            let isPushed = Navigator.that?.push(url) != nil
            if isPushed {
                return true
            } else {
                return (Navigator.that?.open(url)) ?? false
            }
        }
        // 首先需要判斷跳轉(zhuǎn)的目標是否是界面還是處理事件 如果是界面需要: push 如果是事件則需要用:open
        let isPushed = Navigator.that?.push(that) != nil
        if isPushed {
            return true
        } else {
            return (Navigator.that?.open(that)) ?? false
        }
    }
}

這塊其實可以更進一步的封裝判族,比如每次調(diào)整都可以通過正則表達式進行有效性的驗證,或者一些其他路由規(guī)則判斷

借助URLNavigator實現(xiàn)各個模塊的解耦项戴,理論上每個界面都可以實現(xiàn)互相跳轉(zhuǎn)的形帮,在處理商品列表界面的行點擊事件(didSelectRowAt)的時候是由服務端返回的uri字段決定的,具體跳轉(zhuǎn)哪個界面是有服務端決定的肯尺,個人的理解是界面負責產(chǎn)生信號,每個信號都會經(jīng)過復雜的篩選變化又會反應到界面上的躯枢,所有的跳轉(zhuǎn)事件都可以通過 URLNavigator 路由實現(xiàn)则吟,比如邏輯處理、界面跳轉(zhuǎn)等事件

每個模塊都有各自的模塊路由注冊類锄蹂,比如Jacob_router.swift氓仲,包含了該模塊內(nèi)部所有的可路由的界面和事件處理的路由注冊,最后會在主模塊中統(tǒng)一注冊

錯誤處理

監(jiān)控整個 App 的所有錯誤,然后通過一些規(guī)則篩選最后展示給用戶是我們在開發(fā)一個 App 的時候需要考慮處理的敬扛,比如在下拉列表的時候晰洒,發(fā)送網(wǎng)絡請求,這時候網(wǎng)絡請求失敗了啥箭,需要界面上展示網(wǎng)絡錯誤谍珊,并且顯示重新加載的按鈕,或者是如果在調(diào)用相機獲取授權的時用戶沒有授權的時候急侥,需要提示給用戶授權相關的信息砌滞,等等這些邏輯處理都可以通過流的形式處理,在處理用戶網(wǎng)絡錯誤加載失敗的時候坏怪,通過 RxSwift 的一個很簡單的 Api:withLatestFrom就能實現(xiàn)數(shù)據(jù)重新加載贝润,而不需要記住各種復雜的參數(shù)。

根據(jù)錯誤碼的不同進行不同的錯誤邏輯處理铝宵,如下代碼所示

/**
     通過 mikerError 顯示錯誤信息
     202024: 請登錄后再操作
     - parameter error:
     */
    public func toastError(_ error:MikerError){
        if error.code == UtilCore.sharedInstance.toLoginErrorCode {
            self.toastCompletion(error.message){ _ in
                /**
                 *  在這塊 就是跳轉(zhuǎn)到登陸模塊,如果已經(jīng)跳轉(zhuǎn)就不需要直接忽略 否則 先將AppData.sharedInstance.isHasToLoginVc改為true然后再跳轉(zhuǎn)
                 */
                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 {
                 ///表示是生產(chǎn)模式
                let code = "\(error.code)"
                if code.hasPrefix("2") {
                    self.toast(error.message)
                } else {
                    self.toast(UtilCore.sharedInstance.errorMsg)
                }
            }
        }
    }

指令碼

與服務端確認配合確定打掘,通過錯誤碼路由結合能達到一種指令碼的效果,客戶端取到服務端返回的錯誤碼的時候先進行邏輯判斷鹏秋,適配一些規(guī)則尊蚁,如果符合則取服務端返回的uri字段,直接進行路由跳轉(zhuǎn)拼岳,否則走錯誤處理拋出枝誊。這種指令碼可以達到一些客戶端的跳轉(zhuǎn)邏輯交由服務端來控制,比如在注冊完畢之后是跳轉(zhuǎn)首頁還是繼續(xù)補充完詳細信息的這種需求是可以根據(jù)服務端返回的指令碼來決定惜纸。

MVVM 架構設計

一直覺得南峰子翻譯的這兩篇文章挺不錯的雖然是 2014 的文章了叶撒,感興趣的可以看下

另外登陸注冊模塊(Carlisle)是參考RxSwift官方 Demo 設計的,使用 MVVM 架構設計耐版,雖然沒有嚴格遵守上面文章所說的 MVVM 引用層次祠够,不過登陸注冊模塊(Carlisle)還是可以靈活的適用于不同的需求的在簡單修改之后。

Gckit-CLI 的使用

CocoaPods 公共組件模塊可以很方便集成現(xiàn)有的模塊粪牲,但是我們每個業(yè)務都是完全不一樣的古瓤,每個接口返回的 JSON 文件也不一樣,然后我們得手動創(chuàng)建與之對應的 Model腺阳,這些操作完全沒有任何意義但是又是必須的落君,不過現(xiàn)在我們可以使用 Gckit-CLI 一鍵生成對應的所有 Model 實體類,我們只需要把對應的 JSON 文件放到對應的目錄即可亭引,Gckit-CLI 不僅可以生成 Model 文件绎速,ViewModel、ViewController焙蚓、View纹冤、Cell 等各種文件洒宝,并且是一鍵生成,大家可以嘗試使用下萌京,如果覺得可以的話麻煩給一個Star吧 ??雁歌。

Node.js 接口服務

twilight_app 為項目后臺的接口服務,一個客戶端開發(fā)的思維開發(fā)的后臺接口服務 ??知残,功能很簡單靠瞎,如果感興趣的可以下載看下

總結

本文簡單介紹了自己在 Swift 模塊化項目中的一些實踐經(jīng)驗,借助 RxSwift 實現(xiàn) MVVM 框架的設計橡庞,內(nèi)容比較雜较坛,供大家參考,隨著 Swift 5 的發(fā)布扒最,Swift ABI 的穩(wěn)定丑勤,相信會有更多團隊會選擇 Swift 語言開發(fā)自己的 App 的, 周圍認識的很多朋友都說如果嘗試過 Swift 之后就很難再回去用 Objective-C 了吧趣,Swift 本身帶有的很多特性是 Objective-C 不具有的法竞,呀感覺又扯遠了,我個人比較喜歡通過一些工具去實現(xiàn)一些效率方面的提升的强挫,通過模塊化實現(xiàn)代碼的復用岔霸,通過一些腳本工具實現(xiàn)重復無用代碼的自動生成,比如 Model 文件的生成等俯渤,這樣我們通過借助 CocoaPods 和 Gckit-CLI 結合使用呆细,使我們的開發(fā)效率大大提高了,節(jié)省出來的時間我們專注于業(yè)務功能的開發(fā)八匠。

?? 最后感謝您的閱讀!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末絮爷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子梨树,更是在濱河造成了極大的恐慌坑夯,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抡四,死亡現(xiàn)場離奇詭異柜蜈,居然都是意外死亡,警方通過查閱死者的電腦和手機指巡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門淑履,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人藻雪,你說我怎么就攤上這事秘噪。” “怎么了阔涉?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵缆娃,是天一觀的道長。 經(jīng)常有香客問我瑰排,道長贯要,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任椭住,我火速辦了婚禮崇渗,結果婚禮上,老公的妹妹穿的比我還像新娘京郑。我一直安慰自己宅广,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布些举。 她就那樣靜靜地躺著跟狱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪户魏。 梳的紋絲不亂的頭發(fā)上驶臊,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音叼丑,去河邊找鬼关翎。 笑死,一個胖子當著我的面吹牛鸠信,可吹牛的內(nèi)容都是我干的纵寝。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼星立,長吁一口氣:“原來是場噩夢啊……” “哼爽茴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贞铣,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤闹啦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辕坝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窍奋,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年酱畅,在試婚紗的時候發(fā)現(xiàn)自己被綠了琳袄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡纺酸,死狀恐怖窖逗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情餐蔬,我是刑警寧澤碎紊,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布佑附,位于F島的核電站,受9級特大地震影響仗考,放射性物質(zhì)發(fā)生泄漏音同。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一秃嗜、第九天 我趴在偏房一處隱蔽的房頂上張望权均。 院中可真熱鬧,春花似錦锅锨、人聲如沸叽赊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽必指。三九已至,卻和暖如春恕洲,著一層夾襖步出監(jiān)牢的瞬間取劫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工研侣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谱邪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓庶诡,卻偏偏與公主長得像惦银,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子末誓,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容