iOS 基於MVC設計模式的基類設計
前言
-
最近有很多小夥伴,看了筆者這篇 ofollow,noindex">iOS 基於MVVM + RAC + ViewModel-Based Navigation的微信開發(一) 文章後反饋給筆者很多優質性的建議和意見,當然這跟當年筆者寫這篇文章的初衷如出一轍,其根本目的就是拿出來和大家交流分享以及學習知道,希望可以拋磚引玉,取長補短,共同進步。再此,非常感謝大家的積極反饋和批評指導,給了筆者繼續寫文章的動力。
-
iOS 基於MVVM + RAC + ViewModel-Based Navigation的微信開發(一) 這篇文章主要講的是
基於MVVM設計模式
的基類設計,通過基類提供的API和屬性來解決當前產品開發中一些常用的 業務邏輯 和 場景切換 ,以及 快速搭建出專案的基本骨架 ...等等。但是對於剛初學MVVM設計模式
的開發者並不是很友好,可能會導致看完文章一臉懵逼
的下場,然後看完後又不能將其運用到實際專案中去,當然會覺得大失所望呀。當然,這裡筆者建議初學者,可以先看看筆者之前寫的有關於MVVM設計模式
學習的文章,循序漸進,方得始終,有了一定的基礎再來閱讀和學習這篇文章。 -
當然,也有很多重度使用
基於MVC設計模式
開發的以及初學iOS的小夥伴私信筆者,希望我寫一篇關於基於MVC設計模式的常用基(套)類(路)設計
,筆者深感鴨梨山大,並在業餘時間寫了一套筆者開發中常用的基於MVC設計模式的基類設計
套路,才有了本篇文章的誕生。當然還是建議頭鐵的小夥伴先去看看 iOS 基於MVVM + RAC + ViewModel-Based Navigation的微信開發(一) 這篇文章,其BaseClass
的設計說明筆者寫的更加詳細,如此一來,大家瞭解了使用場景後,再反過頭來在看本篇文章,你就會覺得So Easy~。最後希望大家看了以後有所收穫,學以致用。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評指正。
概述
- 這裡筆者還是以
微信
為例,利用筆者常用的基於MVC設計模式
的開發套路開發出微信
的基本骨架。當然這裡需要 特別申明: 以下內容都是筆者在日常開發中比較常用的基於MVC設計模式
的開發套路,希望大家借鑑學習,也歡迎大家說說自己的基於MVC設計模式
的開發套路,也讓筆者借鑑學習學習。 - 本篇文章內容主要側重
基類
的設計和使用,當然筆者會詳細的介紹各個基類的標頭檔案暴露出來的屬性和API
的使用以及具體的使用場景。首先,基類
的出現是為了聚合大量共有的常用業務邏輯
,這樣能極大程度的減少開發者冗餘程式碼的產生,且讓開發者更加專注於自身模組的開發。其次,基類
提供API
讓其子類去重寫,這樣一定程度上保證了開發規範,讓各個開發者寫出易讀、易懂的程式碼。 - 此次,筆者設計的
基類
依然採用的是繼承
的方式來開發微信
的基本骨架,當然,很多小夥伴會問,為何不用協議
的方式?筆者個人認為,協議
過於分散,而繼承
則比較單一。蘿蔔白菜,各有所愛,大家完全可以參考完筆者的基類設計
後,可以自行DIY,寫出自己習慣的套路來即可。
程式碼結構
-
結構
CodeArchitecture.png
- 說明
- Utils: 存放工具類和管理類。例如:分類
Category
... - Vendor: 存放第三方框架。例如:
MJRefresh
... - Macros: 存放常量。例如:巨集(
#define
)定義常量,const
常量,列舉(NS_ENUM
)常量,inline
函式,URL
路徑常量。 - Resource: 存放資原始檔。例如:圖片,
Data
,SQL
,Plist
,Json
等檔案。 - Other: 公有的
Model
,View
,Controller
。例如:MHTextField
... - BaseClass 全域性基類
View
,Model
,ViewController
。用於繼承。
- Utils: 存放工具類和管理類。例如:分類
BaseClass
關於 BaseClass
的設計,筆者主要從 Model
, View
, ViewController
來設計,但是關於 Model
和 View
的基類,這裡建議大家移步 iOS 基於MVVM + RAC + ViewModel-Based Navigation的微信開發(一) 這篇文章關於 Model
和 View
的基類的解釋說明,這裡筆者就不再贅述,這裡著重講的是 ViewController
的基類設計和使用場景。基類檔案結構如下:

BaseClass.png
通過上圖:point_up_2:大家很容易看出 ViewController
的基類在命名跟系統命名類似,無非是把系統 UI
改成 CMH
即可,同時這樣也體現出很大的場景使用性和可讀性。 CMHNavigationController
和 CMHTabBarController
的設計和使用跟 iOS 基於MVVM + RAC + ViewModel-Based Navigation的微信開發(一) 這篇文章的 MHNavigationController
和 MHTabBarController
的設計和使用如出一轍,這裡也就不再贅述了。本篇文章筆者將只詳述: CMHViewController
, CMHTableViewController
, CMHCollectionViewController
, CMHWebViewController
這四個基類的設計說明和使用場景,以及配備大量 Example
來解釋說明基類暴露出來的 屬性和API
。 社會我Mike,人狠話不多 ,基類的使用,筆者一一道來。
CMHViewController
CMHViewController
是整個專案中所有自定義的檢視控制器( ViewController
)的基類,繼承於 UIViewController
。 CMHViewController
主要任務是為其子類提供一些基礎的配置和API,方便子類去配置和重寫,來滿足不同的業務場景。詳情請檢視 CMHViewController.h
檔案內容。 CMHViewController.h
的使用示例都放在 MainFrame
資料夾中。 劃重點 開發者只需要在其子類重寫 init
方法,然後配置一些屬性即可,程式碼如下:
/// 重寫init方法,配置你想要的屬性 - (instancetype)init { self = [super init]; if (self) { /// (是否取消掉當前控制器左滑pop到上一層的功能(棧底控制器無效),NO: 不取消<預設>,YES: 禁止側滑左側返回) self.interactivePopDisabled = YES; /// 禁止側滑場景: /// 1. 主要是防止一些當前控制器的手勢與側滑手勢衝突,比如圖片瀏覽器,圖片貼紙 ...等 /// 2. 不希望側滑返回上一層,比如點選右上角返回按鈕,返回到根檢視 } return self; }
CMHViewController.h
屬性的使用:
/// FDFullscreenPopGesture /// (是否取消掉左滑(側滑)pop到上一層的功能(棧底控制器無效),預設為NO,不取消) @property (nonatomic, readwrite, assign) BOOL interactivePopDisabled;
該屬性控制當前控制器(PS: 當前控制器
是被 Push
進來的)是否取消掉側滑Pop的功能,注意棧底控制器無效。這個側滑返回的功能是iOS開發中比較常見的,且iOS系統在 iOS 7
以後也自帶這種邊緣觸發手勢 UIScreenEdgePanGestureRecognizer
並且其只有一個屬性叫 edges
,用來設定它的觸發邊緣(上、下、左、右、全部),但是隻支援側滑螢幕邊緣才有效。而筆者的側滑是全屏的,當然該功能的實現則得益於 FDFullscreenPopGesture 。其示例程式碼請參照: MainFrame/Example00
/// FDFullscreenPopGesture /// 是否隱藏該控制器的導航欄 預設是不隱藏 (default is NO) @property (nonatomic, readwrite, assign) BOOL prefersNavigationBarHidden;
該屬性控制當前控制器的 導航欄的顯示或隱藏
功能。正常情況下我們常見的顯示或隱藏導航欄程式碼無非是下面:point_down:的程式碼:
- (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; /// 隱藏導航欄 [self.navigationController setNavigationBarHidden:YES animated:YES]; } - (void)viewWillDisappear:(BOOL)animated{ [super viewWillAppear:animated]; /// 顯示導航欄 [self.navigationController setNavigationBarHidden:NO animated:YES]; }
這裡開發者只需要設定 prefersNavigationBarHidden
為 YES or NO
就可以控制顯示或隱藏導航欄。當然該功能的實現則也得益於 FDFullscreenPopGesture 。其示例程式碼請參照: MainFrame/Example01
。
/// 是否隱藏該控制器的導航欄底部的分割線 預設不隱藏 (NO) @property (nonatomic, readwrite, assign) BOOL prefersNavigationBarBottomLineHidden;
該屬性控制當前控制器導航欄底部的分割線的顯示或隱藏,首先系統的導航欄底部本身自帶一個高度為 .5f
的分割線,但是平常開發我們大多數會自定義這條分割線的顏色,來滿足產品樣式開發的統一基調。關於這部分的定義和顯示隱藏邏輯,請參照 CMHNavigationController.h/m
檔案的實現。其示例程式碼請參照: MainFrame/Example02
。
/// IQKeyboardManager /// 是否讓IQKeyboardManager的管理鍵盤的事件 預設是YES(鍵盤管理) @property (nonatomic, readwrite, assign) BOOL keyboardEnable; /// 是否鍵盤彈起的時候,點選其他區域鍵盤掉下 預設是 YES @property (nonatomic, readwrite, assign) BOOL shouldResignOnTouchOutside; /// To set keyboard distance from textField. can't be less than zero. Default is 10.0. /// 鍵盤頂部距離當前響應的textField的底部的距離,預設是10.0f,前提得 `keyboardEnable = YES` 且數值不得小於 0。 @property (nonatomic, readwrite, assign) CGFloat keyboardDistanceFromTextField;
這些屬性都是用來控制鍵盤的相關的事件處理,其底層實現得益於 IQKeyboardManager ,這裡筆者也是抽取了其比較常用的屬性到基類,當然 IQKeyboardManager
更多更牛逼的功能,開發者自行探索和實現哈。其示例程式碼請參照: MainFrame/Example03
。
/// 截圖(Push/Pop Present/Dismiss 過度過程中的縮圖)主要用在過渡動畫裡面 @property (nonatomic, readwrite, strong) UIView *snapshot;
該屬性主要是用來做在(Push/Pop Present/Dismiss)的過渡動畫。關於過渡動畫的使用和說明請參照 WWDC 2013 Session筆記 - iOS7中的ViewController切換 。這裡筆者也提供了兩套過渡動畫的使用,當然有關過渡動畫的問題也可以問我哈,這裡主要突出基類的設計,筆者就不詳細介紹具體的實現內容了,其示例程式碼請參照: MainFrame/Example04
和 MainFrame/Example05
。
/** should request data when viewController videwDidLoad . default is YES*/ /** 是否需要在控制器viewDidLoad後呼叫`requestRemoteData` default is YES*/ @property (nonatomic, readwrite, assign) BOOL shouldRequestRemoteDataOnViewDidLoad;
該屬性控制是否在當前控制器 viewDidLoad
方法呼叫後,是否需要自動呼叫 requestRemoteData
方法(PS:該API下面會說到)。預設情況是YES,而當前控制器只需要重寫基類的 - requestRemoteData
方法即可,無需再在當前控制器 viewDidLoad
方法呼叫後手動呼叫,如果設定為NO,那開發者需要手動呼叫 - requestRemoteData
方法,具體看使用場景。其示例程式碼請參照: MainFrame/Example06
。
/// The callback block. 當Push/Present時,通過block反向傳值 @property (nonatomic, readwrite, copy) void (^callback)(id);
該屬性主要用於反向傳值,這裡使用 block
來代替 delegate
,增加其簡潔性。反向傳值的場景有很多,具體看實際使用場景。
當然正向傳值一般採用 - (instancetype)initWithParams:(NSDictionary *)params;
建立控制器並向其傳值( param
),以及結合以下:point_down:常用的 key
來達到獲取傳過來的的值( param
)。 其示例程式碼請參照: MainFrame/Example07
。
/// The base map of 'params' /// The `params` parameter in `-initWithParams:` method. /// Key-Values's key /// 傳遞唯一ID的key:例如:商品id 使用者id... FOUNDATION_EXTERN NSString *const CMHViewControllerIDKey; /// 傳遞資料模型的key:例如 商品模型的傳遞 使用者模型的傳遞... FOUNDATION_EXTERN NSString *const CMHViewControllerUtilKey; /// 傳遞webView Request的key:例如 webView request... FOUNDATION_EXTERN NSString *const CMHViewControllerRequestKey;
CMHViewController.h
API的使用:
/// ------------ Method ------------ /// Initialization method. This is the preferred way to create a new Controller. /// /// params- The parameters to be passed to Controller. can be nil /// /// Returns a new Controller. - (instancetype)initWithParams:(NSDictionary *)params; /// 基礎配置 (PS:子類可以重寫,但不需要在ViewDidLoad中手動呼叫,但是子類重寫必須要呼叫 [super configure]) - (void)configure; /// 請求遠端資料 /// sub class can override , 但不需要在ViewDidLoad中手動呼叫 ,依賴`shouldRequestRemoteDataOnViewDidLoad = YES` 且不用呼叫 super, 直接重寫覆蓋 - (void)requestRemoteData; /// fetch the local data /// sub class can override ,且不用呼叫 super, 直接重寫覆蓋 /// Returns a local data. - (id)fetchLocalData;
這幾個 API
的使用和說明完全可以看註釋, 但是這幾個 API
的設計的作用,主要是規範子類 API
。子類完全可以根據實際的業務邏輯,去重寫和覆蓋基類的 API
,比如:當前控制器需要獲取遠端資料,則只需要在當前控制器重寫 - (void)requestRemoteData;
方法,如果當前控制器不需要獲取遠端資料,你就不要去重寫 - (void)requestRemoteData;
方法唄。當然寫了也不會懷孕。。其示例程式碼請參照: MainFrame/Example06
。
CMHTableViewController
CMHTableViewController
是整個專案中所有需要顯示列表( UITableView
)的自定義的檢視控制器( ViewController
)的基類,繼承於 CMHViewController
。 CMHTableViewController
主要作用是提供了一個全屏大小的 UITableView
,且懶載入一個數據源 dataSource
,提供了一系列的屬性和API,來滿足現實開發中的常用場景,且使用度非常之高,比如:控制是否支援下拉重新整理和上拉載入,以及暴露下拉重新整理事件和上拉載入事件,子類只需配置相關的屬性和重寫相關的API即可滿足,省去了大量的冗餘程式碼,方便開發者專注於模組功能的開發。詳情請檢視 CMHTableViewController.h
檔案內容。 CMHTableViewController.h
的使用例子都放在 Contacts
資料夾中。
CMHTableViewController.h
屬性的使用:
/// The table view for tableView controller. <自帶全屏tableView,子類可以重新佈局其frame> /// tableView @property (nonatomic, readonly, weak) UITableView *tableView; /// The data source of table view <資料來源懶載入> @property (nonatomic, readonly, strong) NSMutableArray *dataSource; /// 當前頁 defalut is 1 @property (nonatomic, readwrite, assign) NSUInteger page; /// 每一頁的資料 defalut is 20 @property (nonatomic, readwrite, assign) NSUInteger perPage;
首先基類內部提供一個全屏的 tableView
,子類完全可以根據自身的業務場景,定製該 tableView
,比如設定其大小,改變其背景色...等。同時基類懶載入了一個 dataSource
,給子類使用。 page
和 perPage
就具體按照自己後臺規定即可,這裡就無需多言了哈。其示例程式碼請參照: Contacts
資料夾任意一個示例。
/// `tableView` 的內容縮排,default is UIEdgeInsetsMake(64或者88,0,0,0),you can override it @property (nonatomic, readonly, assign) UIEdgeInsets contentInset; /// tableView‘s style defalut is UITableViewStylePlain , 只適合 UITableView 有效 @property (nonatomic, readwrite, assign) UITableViewStyle style;
contentInset
主要影響 tableView.contentInset
罷了,這裡是一個只讀(readonly)屬性,所以子類需要重寫其 get
方法即可。預設值是:如果是iPhone X ,則為 UIEdgeInsetsMake(88,0,0,0)
,若不是,則為 UIEdgeInsetsMake(64,0,0,0)
。其示例程式碼請參照: Contacts/Example12
。
style
則主要影響的是 tableView
的樣式,即: UITableView *tableView = [[UITableView alloc] initWithFrame:[UIScreen mainScreen].bounds style:self.style];
這裡也筆者不過多介紹了。其示例程式碼請參照: Contacts/Example11
。
/// 是否資料是多段 (It's effect tableView's dataSource 'numberOfSectionsInTableView:') defalut is NO,但是跟組頭組尾資料沒任何關聯 @property (nonatomic, readwrite, assign) BOOL shouldMultiSections;
shouldMultiSections
主要影響 tableView
的 UITableViewDataSource
代理方法,即: - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
。但是這裡必須強調的是:它跟組頭組尾的資料顯示沒有關聯,也就是他只能保證 UITableViewCell
是多組的,所以 dataSource
裡面的每一個元素都是一個數組(NSArray)。比如微信的 :發現介面,我的介面,設定介面..。等。其示例程式碼請參照: Contacts/Example15
。
/// 需要支援下來重新整理 defalut is NO @property (nonatomic, readwrite, assign) BOOL shouldPullDownToRefresh; /// 是否預設開啟自動重新整理, YES : 系統會自動呼叫`tableViewDidTriggerHeaderRefresh` NO : 開發人員可以在適當時機手動呼叫 `tableViewDidTriggerHeaderRefresh` @property (nonatomic, readwrite, assign) BOOL shouldBeginRefreshing; /// 需要支援上拉載入 defalut is NO @property (nonatomic, readwrite, assign) BOOL shouldPullUpToLoadMore; /// 是否在上拉載入後的資料,dataSource.count < perPage 提示沒有更多的資料.default is YES 否則 隱藏mi_footer 。 前提是` shouldMultiSections = NO `才有效。 @property (nonatomic, readwrite, assign) BOOL shouldEndRefreshingWithNoMoreData;
上面:point_up_2:這些屬性都跟 上拉載入,下拉重新整理 相關的,這裡的重新整理控制元件用的是 MJRefresh 。筆者在基類屬性設計和命名也比較的直觀易懂,這裡大家完全可以自行參照註釋去學習和使用。這裡筆者就著重說說 shouldEndRefreshingWithNoMoreData
這個屬性,首先該屬性有效的前提是 shouldMultiSections = NO
,否則無效。其次,我們知道上拉載入後的請求的資料可能少於 perPage
,則就說明伺服器已經沒有更多資料了,就無需再去請求資料了,這樣我們需要給使用者友好提示了。關於這幾個屬性的使用示例程式碼請參照 Contacts/Example13 ,Contacts/Example14 ,Contacts/Example16
。
CMHTableViewController.h
API的使用:
/// sub class can override 且 不需要呼叫 [super ....] , 也可以直接呼叫不需要重寫 /// reload tableView data , sub class can override , 等效於 [self.tableView reloadData] - (void)reloadData; /// dequeueReusableCell <複用cell> 子類需重寫,無須呼叫 [super xxx] - (UITableViewCell *)tableView:(UITableView *)tableView dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath; /// configure cell with data <為cell配置模型 , 等效於 cell.model = object> 子類需重寫,無須呼叫 [super configureCell:atIndexPath:withObject:] - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath withObject:(id)object; /// 下拉重新整理事件 子類需重寫,無須呼叫 [super tableViewDidTriggerHeaderRefresh] - (void)tableViewDidTriggerHeaderRefresh; /// 上拉載入事件 子類需重寫,無須呼叫 [super tableViewDidTriggerFooterRefresh] - (void)tableViewDidTriggerFooterRefresh; ///brief 載入結束 這個方法子類只需要在 `tableViewDidTriggerHeaderRefresh`和`tableViewDidTriggerFooterRefresh` 結束重新整理狀態的時候直接呼叫即可,不需要重寫,當然如果不喜歡內部的處理邏輯,你直接重寫即可 ///discussion 載入結束後,通過引數reload來判斷是否需要呼叫tableView的reloadData,判斷isHeader來停止載入 ///param isHeader是否結束下拉載入(或者上拉載入) ///param reload是否需要過載TabeleView - (void)tableViewDidFinishTriggerHeader:(BOOL)isHeader reload:(BOOL)reload;
基類筆者提供以上:point_up_2:幾個API,主要涉及到 介面的重新整理
, 複用Cell的建立
, Cell顯示的模型配置
, 下拉重新整理的事件
, 上拉載入的事件
,以及 結束重新整理控制元件的重新整理狀態
等方法,結合註釋和示例程式,相信小夥伴會很快上手,強烈大家去看看 CMHTableViewController.m
的實現和註釋,這樣能更好的理解上述的屬性和API使用場景。關於這幾個API的使用示例程式碼請參照 Contacts/Example13 ,Contacts/Example14 ,Contacts/Example15,Contacts/Example16
。
CMHCollectionViewController
CMHCollectionViewController
是整個專案中所有需要顯示 UICollectionView
的自定義的檢視控制器( ViewController
)的基類,繼承於 CMHViewController
。 CMHCollectionViewController
主要作用是提供了一個全屏大小的 UICollectionView
,且懶載入一個數據源 dataSource
,提供了一系列的屬性和API,來滿足現實開發中的常用場景。這裡 CMHCollectionViewController
的API和屬性跟 CMHTableViewController
的屬性和API設計的及其類似,且使用也類似,這裡筆者就不再複述了。詳情請檢視 CMHCollectionViewController.h
檔案內容。關於 CMHCollectionViewController.h
的使用例子都放在 Discover
資料夾中。當然這裡著重講的就是那個佈局( collectionViewLayout
)屬性。
/// collectionView 的佈局,預設是 `UICollectionViewFlowLayout` @property (nonatomic, readwrite, strong) UICollectionViewLayout *collectionViewLayout;
首先,我們非常清楚 UICollectionView
最核心的內容也就是其佈局,當然我們在建立 UICollectionView
的時候,即: - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
,必須得傳一個佈局進去,否則就會 Crash
。所以,系統已經為我們提供了一個流水佈局 UICollectionViewFlowLayout
給我們使用,這樣已經完全可以解決大部分的使用場景。當然,一些場景需要自定義佈局,來滿足產品的需求開發,比如瀑布流佈局,電影卡片佈局...等等。這裡筆者就提供了三套佈局來證明該 CMHCollectionViewController
的可行性。只需要在子類裡面重寫 init
方法,並將自定義佈局建立好,賦值給 collectionViewLayout
屬性即可,示例核心程式碼如下:
/// 重寫init方法,配置你想要的屬性 - (instancetype)init { self = [super init]; if (self) { /// create collectionViewLayout CHTCollectionViewWaterfallLayout *layout = [[CHTCollectionViewWaterfallLayout alloc] init]; layout.sectionInset = UIEdgeInsetsMake(10, 15, 10, 15); layout.headerHeight = 0; layout.footerHeight = 0; layout.minimumColumnSpacing = 10; layout.minimumInteritemSpacing = 10; self.collectionViewLayout = layout; self.perPage = 10; /// 支援上下拉載入和重新整理 self.shouldPullUpToLoadMore = YES; self.shouldPullDownToRefresh = YES; } return self; }
關於自定義佈局,大家可以自行百度哈,這裡筆者著重講的是 CMHCollectionViewController
的實用性和拓展性,當然,現實開發中 CMHCollectionViewController
的使用頻率遠遠沒有 CMHTableViewController
的使用度高,當然使用的最多佈局還是 UICollectionViewFlowLayout
。筆者示例程式碼中用到的佈局請參考: CHTCollectionViewWaterfallLayout (瀑布流佈局) 、 UICollectionViewLeftAlignedLayout (左對齊流水佈局)、 Switch" target="_blank" rel="nofollow,noindex">XLCardSwitchFlowLayout (電影卡片佈局)。示例程式碼請參考: Discover/Example20 ,Discover/Example21 ,Discover/Example22,Discover/Example23
。
CMHWebViewController
CMHWebViewController
是整個專案中所有需要顯示 WKWebView
的自定義的檢視控制器( ViewController
)的基類,繼承於 CMHViewController
。 CMHWebViewController
主要作用是提供了一個全屏大小的 WKWebView
,用來載入一些H5介面,當然筆者也提供了一系列的屬性和API,來滿足現實開發中的常用場景。詳情請檢視 CMHWebViewController.h
檔案內容。關於 CMHWebViewController.h
的使用例子都放在 Profile
資料夾中。
/// webView @property (nonatomic, weak, readonly) WKWebView *webView; /// 內容縮排 (64,0,0,0) @property (nonatomic, readonly, assign) UIEdgeInsets contentInset; /// web url quest 如果localFile == YES , 則requestUrl 為本地路徑 ; 反之,requestUrl為遠端url str @property (nonatomic, readwrite, copy) NSString *requestUrl; /// 是否是本地檔案 default is NO @property (nonatomic , readwrite , assign , getter = isLocalFile) BOOL localFile; /// 下拉重新整理 defalut is NO @property (nonatomic, readwrite, assign) BOOL shouldPullDownToRefresh; /// 是否預設開啟自動重新整理, YES : 系統會自動呼叫下拉重新整理事件。NO : 開發人員手動呼叫需要手動拖拽 預設是YES @property (nonatomic, readwrite, assign) BOOL shouldBeginRefreshing; /// 是否取消導航欄的title等於webView的title。預設是不取消,default is NO @property (nonatomic, readwrite, assign) BOOL shouldDisableWebViewTitle; /// 是否取消關閉按鈕。預設是不取消,default is NO @property (nonatomic, readwrite, assign) BOOL shouldDisableWebViewClose; /// messageHandlers: 就是你要註冊的 JS 呼叫 OC 的方法名 @property (nonatomic , readwrite , copy) NSArray <NSString *> *messageHandlers; /// 導航欄高度 預設是 系統導航欄的高度 @property (nonatomic , readwrite , assign) CGFloat navigationBarHeight;
contentInset
、 shouldPullDownToRefresh
、 shouldBeginRefreshing
跟 CMHTableViewController
使用場景一模一樣,這裡筆者就講講 shouldDisableWebViewTitle
和 shouldDisableWebViewClose
兩個屬性的作用和使用場景。
shouldDisableWebViewTitle
: 是否取消導航欄的 title
等於 webView
的 title
。預設做法是 MHWebViewController
及其子類的導航欄 title
為 WebView
的 title
,而不是 MHViewModel
的 title
屬性。即控制器通過 KVO
的形式監聽 WKWebView
的 title
屬性,從而設定導航欄的 title
, self.navigationItem.title = self.webView.title
。但是可能有幾個 H5
介面想要設定導航欄的 title
為 MHViewModel
的 title
屬性,正所謂需求拉動生成,所以就產生了該屬性。
shouldDisableWebViewClose
:是否導航欄左側取消關閉按鈕,預設是不取消。這主要是為了解決點選網頁裡面的連結繼續載入另一個網頁,如果重複前面的步驟幾次,則網頁層次就會非常的深(A - B - C - D - E ...)。如果我們點選 MHWebViewController
導航欄的左側的返回按鈕,其預設做法是返回到上一個網頁( [self.webView goBack]
),這樣由於前面的步驟,導致網頁層次過深,我們需要點選多次返回按鈕,才能返回到最初的網頁,繼而才能返回上一個介面,這樣使用者操作過多,使用者體驗下降(PS:幹著程式猿的活,抄著產品經理的心)。
requestUrl
和 localFile
: requestUrl
是 H5
需要載入的地址, localFile
則用來區分地址是否為 網路地址
和 本地地址
。 網路地址
的載入這裡沒什麼好講的,直接呼叫 WKWebView
的 - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
即可。但是載入 本地地址
的時候,則需要區分手機系統是否是 iOS9.0以上的系統
,這裡講講, iOS9.0
以下的版本,如果單純的用 - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
來載入 本地地址
是有問題的。具體問題如下:
當使用loadRequest來讀取本地的HTML時,WKWebView是無法讀取成功的,後臺會出現如下的提示:
Could not create a sandbox extension for /
原因是WKWebView是不允許通過loadRequest的方法來載入本地根目錄的HTML檔案。
而在iOS9的SDK中加入了以下方法來載入本地的HTML檔案:
[WKWebView loadFileURL:allowingReadAccessToURL:]
但是在iOS9以下的版本是沒提供這個便利的方法的。以下為解決方案的思路,就是在iOS9以下版本時,先將本地HTML檔案的資料copy到tmp目錄中,然後再使用loadRequest來載入。但是如果在HTML中加入了其他資原始檔,例如js,css,image等必須一同copy到temp中。這個是最蛋疼的事情了。
所以解決方案如下:
- (void)configure{ [super configure]; /// 容錯處理 if (MHStringIsNotEmpty(self.requestUrl) && !self.isLocalFile){/// 網路 //格式化含有中文的url self.requestUrl =(NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self.requestUrl, (CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]", nil, kCFStringEncodingUTF8)); NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.requestUrl]]; /// 載入請求資料 [self.webView loadRequest:request]; }else if (MHStringIsNotEmpty(self.requestUrl) && self.isLocalFile){ /// 本地 /// 本地分 ios9.0以下 和 ios9.0以上處理方式 /// https://www.jianshu.com/p/ccb421c85b2e /// https://blog.csdn.net/xinshou_caizhu/article/details/72614584 /// https://blog.csdn.net/wojiaoqiaoxiaoqiao/article/details/79876904 NSURL *fileURL = [NSURL fileURLWithPath:self.requestUrl]; if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) { // iOS9. One year later things are OK. [self.webView loadFileURL:fileURL allowingReadAccessToURL:fileURL]; } else { // iOS8. Things can be workaround-ed //Brave people can do just this fileURL = [self _fileURLForBuggyWKWebView8:fileURL]; NSURLRequest *request = [NSURLRequest requestWithURL:fileURL]; [self.webView loadRequest:request]; } } } /// 9.0以下將資料夾copy到tmp目錄 - (NSURL *)_fileURLForBuggyWKWebView8:(NSURL *)fileURL { NSError *error = nil; if (!fileURL.fileURL || ![fileURL checkResourceIsReachableAndReturnError:&error]) { return nil; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *temDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; [fileManager createDirectoryAtURL:temDirURL withIntermediateDirectories:YES attributes:nil error:&error]; NSURL *dstURL = [temDirURL URLByAppendingPathComponent:fileURL.lastPathComponent]; [fileManager removeItemAtURL:dstURL error:&error]; [fileManager copyItemAtURL:fileURL toURL:dstURL error:&error]; return dstURL; }
當然iOS9.0以下 WKWebView
的坑點還不止這些,有興趣的童鞋可以看看下面:point_down:的坑。
- https://www.jianshu.com/p/ccb421c85b2e
- https://blog.csdn.net/xinshou_caizhu/article/details/72614584
- https://blog.csdn.net/wojiaoqiaoxiaoqiao/article/details/79876904
messageHandlers:
主要用來處理 JS互動
的,裡面裝著 webView
要註冊的 JS
呼叫 OC
的方法名。 JS
與 原生
之間的互動,想必大家並不陌生,這裡筆者就簡單的說說,這裡是基於 WKWebView
提供的API來實現的。示例程式碼請參考: Profile/Example35
.
OC 呼叫 JS :
利用系統提供的API,直接呼叫 - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
API即可。
JS 呼叫 OC
:這種場景是開發中比較常見得需求,當然利用系統提供的 API
和 代理
也是非常方便。主要分為以下兩步:
/*! @abstract Adds a script message handler. @param scriptMessageHandler The message handler to add. @param name The name of the message handler. @discussion Adding a scriptMessageHandler adds a function window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all frames. */ /// 第一步:註冊JS呼叫OC的方法名, name : 就是JS呼叫的方法名 - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name; /*! @abstract Invoked when a script message is received from a webpage. @param userContentController The user content controller invoking the delegate method. @param message The script message received. */ /// 第二步:實現<WKScriptMessageHandler>協議中方法,其中message就是JS回撥的值。 /// message.name : 就是你第一步註冊的方法名,通過判斷方法名,來處理不同的事件 /// message.body:就是JS給原生傳的引數 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
特別說明:使用 - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name
會導致迴圈引用,從而導致控制器釋放不掉而導致記憶體洩漏,具體原因如下:
這裡WKUserContentController物件的addScriptMessageHandler方法的scriptMessageHandler引數傳入了將控制器本身(猜測addScriptMessageHandler將會對scriptMessageHandler引數傳入的物件做強引用,這點開發文件沒有說明),而控制器又強引用了webView,然後webView又強引用了configuration,configuration又強引用了WKUserContentController物件,所以導致了引用迴圈,從而導致控制器不被釋放的問題.
具體解決方案,可以參照下面:point_down:來實現,這裡筆者就不過多贅述了:
- https://www.jianshu.com/p/3cc26c48b7e7
- https://www.jianshu.com/p/6ba2507445e4
- http://blog.cocosdever.com/2016/03/07/WKWebView-JS%E4%BA%92%E4%BA%A4%E5%BC%80%E5%8F%91%E4%B8%8E%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/
具體程式碼實現如下:
/// CoderMikeHe Fixed Bug : 防止迴圈引用,以及重複新增handler @interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler> @property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate; - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate; @end @implementation WeakScriptMessageDelegate - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate{ self = [super init]; if (self) { _scriptDelegate = scriptDelegate; } return self; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; } - (void)dealloc{ MHDealloc; } @end /// 在CMHWebViewController的使用 - (void)dealloc{ MHDealloc; /// remove observer ,otherwise will crash [_webView stopLoading]; /// CoderMikeHe Fixed Bug :移除掉JS呼叫OC的方法,否則迴圈引用 for (NSString * name in _messageHandlers) { [_webView.configuration.userContentController removeScriptMessageHandlerForName:name]; } [_webView stopLoading]; _webView.scrollView.delegate = nil; _webView.navigationDelegate = nil; _webView.UIDelegate = nil; _webView = nil; } - (void)configure{ [super configure]; /// 註冊 JS呼叫OC的方法 for (NSString * name in self.messageHandlers) { [self.webView.configuration.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:name]; } }
總結
以上內容就是筆者在產品開發中,若使用 MVC設計模式
來開發產品的前提下,比較常用一套開發套路。首先,這只是筆者喜愛的開發套路,並不能滿足所有開發者的業務場景,大家只需要把有用或有效的內容拿過來參照參照,且能夠完美運用到自身專案的架構中去,這樣筆者就非常欣慰了。其次,如果大家有許多好點子或建議,請及時反饋( 評論or私信 )給筆者,看能否加在基類裡面,更好的去造福於廣大初學的開發者。最後,整體專案的基類設計,無論在屬性命名,還是API設計都有比較強的邏輯性和指向性,當然筆者能力有限,並沒有設計成非常的高大上,而是非常的易讀易懂,大家可以結合註釋以及使用示例更好的去理解和使用,爭取早日上手和拓展使用。
當然這裡主要講的基類的標頭檔案的內容,具體的實現邏輯還是需要大家去基類的實現檔案 .m
去學習和理解,當然大家主要看那些筆者用 CoderMikeHe Fixed Bug
標識的部分,大家也可以全域性搜尋 CoderMikeHe Fixed Bug
欄位,這些修復Bug的內容,才是整篇基類的核心所在,當然,筆者依然不敢保證還是否有Bug的出現。有問題,及時交流即可。
最後講講關於開發者在自定義控制器時,基類繼承的選取問題,建議如下。
- 如果你自定義的控制器不需要顯示
tableView
、collectionView
、webView
,只想一個簡簡單單控制器,那麼就直接繼承於CMHViewController
即可,比如微信登入模組
。 - 如果你自定義的控制器需要使用到
tableView
來展示列表資料,那麼就直接繼承於CMHTableViewController
即可,比如微信發現模組
。 - 如果你自定義的控制器需要
collectionView
來展示類似九宮格的資料,那麼就直接繼承於CMHCollectionViewController
即可,比如自定義手機相簿模組
。 - 如果你自定義的控制器需要載入
H5
頁面,那麼就直接繼承於CMHWebViewController
即可或者沒有H5互動的情況直接使用CMHWebViewController
,比如微信使用幫助模組
。 - 究竟繼承哪個基類,請根據自身模組的使用場景,靈活選取即可。
期待
- 文章若對您有點幫助,請給個喜歡:heart:,畢竟碼字不易;若對您沒啥幫助,請給點建議:heartpulse:,切記學無止境。
- 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
- GitHub地址: https://github.com/CoderMikeHe
- 原始碼地址:
MHDevelopExample目錄中的Architecture資料夾中 < 特別強調: 使用前請全域性搜尋CMHDEBUG
欄位並將該 巨集 置為1
即可,預設是0
>