iOS 基于MVC設計模式的基類設計

前言
  • 最近有很多小伙伴,看了筆者這篇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,寫出自己習慣的套路來即可。
代碼結構
  1. 結構


    CodeArchitecture.png
  2. 說明
    • Utils:存放工具類和管理類。例如:分類Category...
    • Vendor:存放第三方框架。例如:MJRefresh...
    • Macros:存放常量。例如:宏(#define)定義常量,const常量,枚舉(NS_ENUM)常量,inline函數,URL路徑常量。
    • Resource:存放資源文件。例如:圖片,DataSQLPlistJson等文件。
    • Other: 公有的ModelViewController。例如:MHTextField...
    • BaseClass 全局基類ViewModelViewController。用于繼承。
BaseClass

關于BaseClass的設計,筆者主要從ModelViewViewController來設計,但是關于ModelView的基類,這里建議大家移步iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(一)這篇文章關于ModelView的基類的解釋說明,這里筆者就不再贅述,這里著重講的是ViewController的基類設計和使用場景。基類文件結構如下:

BaseClass.png

通過上圖??大家很容易看出ViewController的基類在命名跟系統命名類似,無非是把系統UI改成CMH即可,同時這樣也體現出很大的場景使用性和可讀性。CMHNavigationControllerCMHTabBarController的設計和使用跟iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信開發(一)這篇文章的MHNavigationControllerMHTabBarController的設計和使用如出一轍,這里也就不再贅述了。本篇文章筆者將只詳述:CMHViewControllerCMHTableViewControllerCMHCollectionViewControllerCMHWebViewController這四個基類的設計說明和使用場景,以及配備大量Example來解釋說明基類暴露出來的屬性和API社會我Mike,人狠話不多,基類的使用,筆者一一道來。

CMHViewController

CMHViewController 是整個項目中所有自定義的視圖控制器(ViewController)的基類,繼承于UIViewControllerCMHViewController主要任務是為其子類提供一些基礎的配置和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;

該屬性控制當前控制器的導航欄的顯示或隱藏功能。正常情況下我們常見的顯示或隱藏導航欄代碼無非是下面??的代碼:

- (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];
}

這里開發者只需要設置prefersNavigationBarHiddenYES 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/Example04MainFrame/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),以及結合以下??常用的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.hAPI的使用:

/// ------------ 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)的基類,繼承于CMHViewControllerCMHTableViewController主要作用是提供了一個全屏大小的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,給子類使用。 pageperPage就具體按照自己后臺規定即可,這里就無需多言了哈。其示例代碼請參照: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主要影響tableViewUITableViewDataSource代理方法,即:- (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;

上面??這些屬性都跟上拉加載,下拉刷新相關的,這里的刷新控件用的是MJRefresh。筆者在基類屬性設計和命名也比較的直觀易懂,這里大家完全可以自行參照注釋去學習和使用。這里筆者就著重說說shouldEndRefreshingWithNoMoreData這個屬性,首先該屬性有效的前提是shouldMultiSections = NO,否則無效。其次,我們知道上拉加載后的請求的數據可能少于perPage,則就說明服務器已經沒有更多數據了,就無需再去請求數據了,這樣我們需要給用戶友好提示了。關于這幾個屬性的使用示例代碼請參照Contacts/Example13 ,Contacts/Example14 ,Contacts/Example16


CMHTableViewController.hAPI的使用:

/// 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;

基類筆者提供以上??幾個API,主要涉及到界面的刷新復用Cell的創建Cell顯示的模型配置下拉刷新的事件上拉加載的事件,以及結束刷新控件的刷新狀態等方法,結合注釋和示例程序,相信小伙伴會很快上手,強烈大家去看看CMHTableViewController.m的實現和注釋,這樣能更好的理解上述的屬性和API使用場景。關于這幾個API的使用示例代碼請參照Contacts/Example13 ,Contacts/Example14 ,Contacts/Example15,Contacts/Example16

CMHCollectionViewController

CMHCollectionViewController 是整個項目中所有需要顯示UICollectionView的自定義的視圖控制器(ViewController)的基類,繼承于CMHViewControllerCMHCollectionViewController主要作用是提供了一個全屏大小的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(左對齊流水布局)、XLCardSwitchFlowLayout(電影卡片布局)。示例代碼請參考:Discover/Example20 ,Discover/Example21 ,Discover/Example22,Discover/Example23

CMHWebViewController

CMHWebViewController 是整個項目中所有需要顯示WKWebView的自定義的視圖控制器(ViewController)的基類,繼承于CMHViewControllerCMHWebViewController主要作用是提供了一個全屏大小的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;

contentInsetshouldPullDownToRefreshshouldBeginRefreshingCMHTableViewController使用場景一模一樣,這里筆者就講講shouldDisableWebViewTitleshouldDisableWebViewClose兩個屬性的作用和使用場景。
shouldDisableWebViewTitle: 是否取消導航欄的title等于webViewtitle。默認做法是MHWebViewController及其子類的導航欄titleWebViewtitle,而不是MHViewModeltitle屬性。即控制器通過KVO的形式監聽WKWebViewtitle屬性,從而設置導航欄的titleself.navigationItem.title = self.webView.title。但是可能有幾個H5界面想要設置導航欄的titleMHViewModeltitle屬性,正所謂需求拉動生成,所以就產生了該屬性。
shouldDisableWebViewClose:是否導航欄左側取消關閉按鈕,默認是不取消。這主要是為了解決點擊網頁里面的鏈接繼續加載另一個網頁,如果重復前面的步驟幾次,則網頁層次就會非常的深(A - B - C - D - E ...)。如果我們點擊MHWebViewController導航欄的左側的返回按鈕,其默認做法是返回到上一個網頁([self.webView goBack]),這樣由于前面的步驟,導致網頁層次過深,我們需要點擊多次返回按鈕,才能返回到最初的網頁,繼而才能返回上一個界面,這樣用戶操作過多,用戶體驗下降(PS:干著程序猿的活,抄著產品經理的心)。
requestUrllocalFilerequestUrlH5需要加載的地址,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以上處理方式
        /// http://www.lxweimin.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的坑點還不止這些,有興趣的童鞋可以看看下面??的坑。

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對象,所以導致了引用循環,從而導致控制器不被釋放的問題.

具體解決方案,可以參照下面??來實現,這里筆者就不過多贅述了:

具體代碼實現如下:

/// 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的出現。有問題,及時交流即可。

最后講講關于開發者在自定義控制器時,基類繼承的選取問題,建議如下。

  • 如果你自定義的控制器不需要顯示tableViewcollectionViewwebView,只想一個簡簡單單控制器,那么就直接繼承于CMHViewController即可,比如微信登錄模塊
  • 如果你自定義的控制器需要使用到tableView來展示列表數據,那么就直接繼承于CMHTableViewController即可,比如微信發現模塊
  • 如果你自定義的控制器需要collectionView來展示類似九宮格的數據,那么就直接繼承于CMHCollectionViewController即可,比如自定義手機相冊模塊
  • 如果你自定義的控制器需要加載H5頁面,那么就直接繼承于CMHWebViewController即可或者沒有H5交互的情況直接使用CMHWebViewController,比如微信使用幫助模塊
  • 究竟繼承哪個基類,請根據自身模塊的使用場景,靈活選取即可。
期待
  1. 文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
  2. 針對文章所述內容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
  3. GitHub地址:https://github.com/CoderMikeHe
  4. 源碼地址:
    MHDevelopExample目錄中的Architecture文件夾中 <特別強調: 使用前請全局搜索 CMHDEBUG 字段并將該置為 1即可,默認是0 >
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數據庫組件 SD...
    陽明AGI閱讀 16,003評論 3 119
  • 1、“不是我不想學,而是我無法集中注意力”——根源:專注力不足,干擾太多,電腦游戲, 2、“不是我不想學,而是我沒...
    春天的蔓藤梅閱讀 373評論 0 0
  • 這幾天,地鐵哺乳事件又一次引發了全民討論,母親在公共場所給孩子哺乳,究竟是該被譴責的不文明行為,還是該被理解的母性...
    時尚show閱讀 301評論 1 3
  • 不知道你想做什么,但是我知道我想做什么。 帶上背包我們一起去旅行。 不要去繁華的地方,怕你心亂,到了那里就不肯再陪...
    曲上未合閱讀 277評論 0 0
  • 執行安裝命令: 然后回車。結果為 查詢軟件 如搜索WeChat 結果為: 假如不知道軟件的名字,先查詢下包的名字,...
    哈利波特會魔法閱讀 601評論 0 0