Swift + RxSwift MVVM 模塊化項目實踐

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

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

項目介紹

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

Demo 效果演示

App 架構設計

最頂層為 主工程,包含一些簡單的配置、路由注冊等,相當于一個空殼,模塊化之后需要注意的一點是:模塊的版本管理,每次發(fā)版一定要記錄好每個模塊的版本號等,否則代碼回退、Bug 排查是一件很困難的事,我們主工程中會記錄每次發(fā)版時各個模塊的版本號的。接下來就是業(yè)務層,包括各個不同的業(yè)務模塊,這些模塊之間的調用是通過路由實現的,不能存在引用關系的,每個模塊會依賴一個上下文模塊和項目配置模塊,上下文模塊主要是管理用戶對象等用戶權限相關的事,項目配置模塊主要是整體 App 的一些配置數據、以及主題顏色和一些第三方 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代表點擊重新加載的事件,經過參數格式化、發(fā)送網絡請求、數據解析等數據處理,最后只需關注解析成功之后的 Model 數據然后更新 UI 界面。

公用模塊

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

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

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

RxSwift 的使用

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

網絡請求

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
        }
    }
}

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

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

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

錯誤處理

監(jiān)控整個 App 的所有錯誤,然后通過一些規(guī)則篩選最后展示給用戶是我們在開發(fā)一個 App 的時候需要考慮處理的,比如在下拉列表的時候,發(fā)送網絡請求,這時候網絡請求失敗了,需要界面上展示網絡錯誤,并且顯示重新加載的按鈕,或者是如果在調用相機獲取授權的時用戶沒有授權的時候,需要提示給用戶授權相關的信息,等等這些邏輯處理都可以通過流的形式處理,在處理用戶網絡錯誤加載失敗的時候,通過 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)
                }
            }
        }
    }

指令碼

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

MVVM 架構設計

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

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

Gckit-CLI 的使用

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

Node.js 接口服務

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

總結

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

?? 最后感謝您的閱讀!

原文地址:

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 本文主要介紹個人在 Swift 項目開發(fā)中的一些實踐經驗,供大家所借鑒或者探討。 提高開發(fā)效率,降低 Bug 發(fā)生...
    seongbrave閱讀 1,645評論 1 9
  • 路由是實現模塊間解耦的一個有效工具。如果要進行組件化開發(fā),路由是必不可少的一部分。目前iOS上絕大部分的路由工具都...
    黑超熊貓zuik閱讀 4,088評論 8 52
  • 文/逐夢水鄉(xiāng) 我們這里,自古就有四月八漲大水的說法。看來氣象專家的預測是對的,極端天氣的說法得到了印證。 人們都說...
    逐夢水鄉(xiāng)閱讀 1,293評論 32 45
  • 陌生女:韓一成! 韓一成:額,你在跟蹤我嗎,你想干嘛? 陌生女:不是的,我這不算跟蹤你。 韓一成:那到底為什么跟著...
    Xfor阿妤閱讀 376評論 3 5
  • 立秋了。 我最愛的季節(jié) 馮唐有句詩:秋天短到沒有,你我短到回不到過去。幸而,在秋天,有你與我一同賞味人生;在秋天,...
    月羊不是羊閱讀 189評論 0 0

友情鏈接更多精彩內容