Swift + RxSwift MVVM 模組化專案實踐

seongbrave發表於2019-04-14

本文主要介紹個人在 Swift 專案開發中的一些實踐經驗,供大家所借鑑或者探討。

提高開發效率,降低 Bug 發生率,是我們每個開發所追隨的目標。個人認為通過 CocoaPods 實現模組化元件化,積累適合的元件模組,重複利用公用模組,不僅可以提高開發效率並且可以有效的降低 Bug 的發生,另外可以藉助 Gckit-CLI 等指令碼工具降低重複無用的程式碼編寫,進一步提高開發效率,降低低階錯誤的發生,本文以下內容主要講解個人通過 CocoaPods 結合 Gckit-CLI 實現開發效率的最大化的一些專案實踐

專案介紹

Twilight,專案取自暮光之城電影名 所有的資源都已經開源到 Github 上了,包括服務端的介面專案

Demo 效果演示

Swift + RxSwift MVVM 模組化專案實踐

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 簡單修改完成的。

上下文模組(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 結合使用,使我們的開發效率大大提高了,節省出來的時間我們專注於業務功能的開發。

? 最後感謝您的閱讀!

相關文章