# MVC
?框架的所有代碼結構整合都是采用MVC的基礎架構,這也是蘋果iOS系統的基本架構。Controller作為關鍵角色負責View層和Data層的數據流交換,現在市面上流行很多架構優劣的爭論(例如討論比較多的MVVM),但是在我看來,**不論是那種架構都是以MVC為基礎的,然后不斷的劃分集體職責,細化得來的。所以能夠清楚的掌握職責劃分,將視圖、邏輯和數據三者聯系起來,易用并方便維護,那就可以了,無所謂框架的優劣**。
#代碼庫管理
?在本框架的代碼管理中使用的Cocoapod作為第三方依賴的統一管理模塊。
在iOS的開發過程中會經常使用到第三方開源庫,或者跟其他團隊合作的時候有些公告的代碼模塊需要很幾個團隊使用的情況,更復雜的情況是某些第三方庫還會再引用其他的第三方庫,所以手動去一個個的下載并配置每個第三庫的使用是一件十分費力不討好的事情,而且還可能會出現各種的編譯錯誤。另外如果項目中的類庫需要更新,就需要手動把就的文件刪除在重新下載新版本的類庫然后再添加到工程中去,以上兩種情況是類庫管理中經常出現的問題,因此我們引入管理工具Cocoapod來簡明清楚的管理框架中使用的第三方類庫和自定義類庫
CocoaPod資源[https://github.com/CocoaPods/CocoaPods](https://github.com/CocoaPods/CocoaPods)
#網絡核心模塊
?在本框架中網絡核心模塊對`AFNetworking`**進行了抽象的封裝,抽取**`AFNetworking`的常用的POST和GET請求,并在此基礎上添加網絡機密功能。
主要包塊以下幾個主要的功能:
1、表單數據上傳功能,主要用于上傳富文本文件
```objective-c
/**
*上傳
*
*@param datas可以同時上傳多個,datas為NSDictionary的集合,NSDictionary中必需包含data, name, fileName, mimeType
*/
-(void)requestWithMultiDatas:(NSArray*)datas params:(NSDictionary*)params;
```
2、非加密POST請求
```objective-c
/*請求非加密-參數類型只能是字符串或者數據字典*/
-(void)requestWithParams:(id)templates;
```
3、加密POST請求,報文體采用AES加密的方式,密鑰由服務端提供
```objective-c
/*請求加密*/
-(void)requestWithAESEncrpytParams:(NSDictionary*)params token:(NSString*)token;
```
4、Get請求(Get請求均不加密)
```objective-c
/*請求加密*/
-(void)requestWithAESEncrpytParams:(NSDictionary*)params token:(NSString*)token;
```
?網絡請求模塊作為BaseViewController的基礎功能,因此在實際使用中由Controller來處理HTTP請求,同時對數據進行處理和封裝,然后展示給View層。
#資源加載
?資源加載在這里特指App讀取和加載資源圖片,在實際開發中需要大量使用圖片來顯示App的視覺效果,但是根據硬件的物理尺寸不同,在讀取資源的時候分為@2x和@3x兩種圖片。前者針對非5.5英寸的屏幕尺寸,后者是針對5.5英寸的屏幕尺寸。
?理論上說每個App都應真多不同的屏幕尺寸分別存在一套@2x和@3x的圖片,但是在實際使用中兩套圖片的存在會大大的增加App的體積這是不可取的,因此此框架在資源加載額時候統一使用4.7英寸**(@2x圖)**作為標準,向上長寬分別擴大1.1倍來適配5.5英寸的屏幕尺寸,向下縮小1.08倍來適配4英寸的屏幕尺寸。
-把圖片資源根據需要加載的模塊放在根目錄的res文件夾下統一管理
-通過打包腳本build_plist.sh將res下的資源名稱組織成plist文件,其中圖片索引跟世界@2x圖片名稱一一對應。
腳本文件build_plist.sh
```shell
#根據res下文件結構,構建config.plist
/usr/libexec/PlistBuddy -c "Delete :res" res/resconfig.plist
find res | grep @2x.png$ | while read line
do
if [ "$line" ]
then
path1=${line#res/}
path2=${path1%@2x.png}
path3=${path2//\//_}
image_path=${line%@2x.png}
/usr/libexec/PlistBuddy -c "Add :res:$path3 string \"/$image_path.png\""res/resconfig.plist
fi
done
#/usr/libexec/PlistBuddy -c "Print" res/resconfig.plist
```
通過資源加載UIImageManager類的imageName方法來根據圖片索引加載實際對應的圖片資源
```objective-c
/*@param imageKey imageKey description
*
*@return UIImage
*/
+ (UIImage *)imageNamed:(NSString *)imageKey
{
NSString *imagePath = [[QHResManager sharedInstance] pathForImage:imageKey];
UIImage *tmpImage = [UIImage imageWithContentsOfFile:imagePath];
if (tmpImage == nil)
{
imagePath = [imagePath stringByReplacingOccurrencesOfString:@".png" withString:@"@2x.png"];
tmpImage = [UIImage imageWithContentsOfFile:imagePath];
}
return tmpImage;
}
```
*資源加載模塊的優勢:*
> 1、圖片資源統一管理,資源分類清晰
>
> 2、通過圖片索引加載圖片,避免頻繁書寫@2x和@3x,減少代碼冗余
>
> 3、自動適應屏幕的尺寸,免除在使用中需要手動計算圖片大小做屏幕自適應
#日志Log模塊
日志系統簡單來說就是在程序運行中通過NSLog來打印出來,上下文中關鍵的信息,方便進行程序調試和問題的追蹤,但是如果僅僅是簡單的打印一句話在實際的使用中是無法目標大型應用的要求的,一個成熟的Log系統需要如下功能:
-可以設定Log等級
-可以積攢到一定量的log后,一次性發送給服務器,絕對不能打一個Log就發一次
-可以一定時間后,將未發送的log發送到服務器
-可以在App切入后臺時將未發送的log發送到服務器
如果要實現上述的要求,在框架中采用CocoaLumberjack來輔助搭建自己的日志系統。
CocoaLumberjack最早是由[Robbie Hanson ](https://github.com/robbiehanson)開發的日志庫,可以在iOS和MacOSX開發上使用。其簡單,快讀,強大又不失靈活。它自帶了幾種log方式,分別是:
- DDASLLogger將log發送給蘋果服務器,之后在Console.app中可以查看
- DDTTYLogger將log發送給Xcode的控制臺
- DDFileLogger講log寫入本地文件
CocoaLumberjack打一個log的流程大概就是這樣的:

所有的log都會發給DDLog對象,其運行在自己的一個GCD隊列(GlobalLoggingQueue),之后,DDLog會將log分發給其下注冊的一個或多個Logger,這步在多核下是并發的,效率很高。每個Logger處理收到的log也是在它們自己的GCD隊列下(loggingQueue)做的,它們詢問其下的Formatter,獲取Log消息格式,然后最終根據Logger的邏輯,將log消息分發到不同的地方。
因為一個DDLog可以把log分發到所有其下注冊的Logger下,也就是說一個log可以同時打到控制臺,打到遠程服務器,打到本地文件,相當靈活。
CocoaLumberjack支持Log等級:
```objective-c
typedef NS_OPTIONS(NSUInteger, DDLogFlag) {
DDLogFlagError= (1 << 0), // 0...00001
DDLogFlagWarning= (1 << 1), // 0...00010
DDLogFlagInfo= (1 << 2), // 0...00100
DDLogFlagDebug= (1 << 3), // 0...01000
DDLogFlagVerbose= (1 << 4)// 0...10000
};
typedef NS_ENUM(NSUInteger, DDLogLevel) {
DDLogLevelOff= 0,
DDLogLevelError= (DDLogFlagError),// 0...00001
DDLogLevelWarning= (DDLogLevelError| DDLogFlagWarning), // 0...00011
DDLogLevelInfo= (DDLogLevelWarning | DDLogFlagInfo),// 0...00111
DDLogLevelDebug= (DDLogLevelInfo| DDLogFlagDebug),// 0...01111
DDLogLevelVerbose= (DDLogLevelDebug| DDLogFlagVerbose), // 0...11111
DDLogLevelAll= NSUIntegerMax// 1111....11111 (DDLogLevelVerbose plus any other flags)
};
```
DDLogLevel定義了全局的log等級,DDLogFlag是我們打log時設定的log等級,CocoaLumberjack會比較兩者,如果flag低于level,則不會打log。
DDLogger協議定義了logger對象需要遵從的方法和變量,為了方便使用,其提供了DDAbstractLogger對象,我們只需要繼承該對象就可以自定義自己的logger。對于第二點和第三點需求,我們可以利用DDAbstractDatabaseLogger,其也是繼承自DDAbstractLogger,并在其上定義了saveThreshold, saveInterval等控制參數。這個logger本身是針對寫入數據庫的log設計的,我們也可以利用它這幾個參數,實現我們上面所提的需求的第二和第三點。
對于第二點,設定_saveThreshold值即可,比如如果希望積攢1000條log再一次性發送,就賦值1000.
對于第三點,設定_saveInterval,比如如果希望每分鐘發送一次,就設定60.
由此,CocoaLumberjack已經實現了需求中的1、2、3點,我們要做的無非是自定義Logger和Formatter,將log的最終去處改為發送到我們自己的服務器中。
而第四點,我們可以監聽UIApplicationWillResignActiveNotification事件,當觸發時,手動調用logger的db_save方法,發送數據給服務器。
#工具處理Util
工具類處理主要包括圖片處理、文件系統、日期、加解密、時間、字符串等常用的工具函數,具體的使用可以參考對應的頭文件里邊的函數注釋。
QHFileUtil文件處理
QHImageUtil圖片處理
QHDateUtil日期處理
QHTimerUtil時間處理
/Encrypt目錄提供AES加解密算法、RSA加解密算法、Base64編碼、MD5編碼、SHA1編碼等算法
# BaseUI
框架的baseUI主要是指的三個提供基礎服務的組件,分別是控制器基類BaseViewController、導航控制器基類BaseNavigationController和WebView控制器BaseWebViewController。
## BaseViewController
該控件主要對controller的基礎服務進行封裝,在實際使用過程中所有的業務邏輯的controller都應該從此類進行繼承。它主要封裝如下功能供子類使用:
> - controller樣式定義
> -網絡模塊的調用,以及網絡成功、失敗、處理中回調函數的處理
> -頁面切換返回操作pop
> -公告遮罩的顯示、隱藏和等待動畫
> -多線程
> -切換手勢操作
以上功能在基類中給出詳細的實現,且這些功能都是子類很頻繁使用的業務邏輯,將這些業務邏輯在基類中實現可以更好實現代碼的復用,減少程序冗余
## BaseNavigationController
BsaeNavigationController繼承自UINavigationController,除了提供必要的頁面跳轉邏輯之外,本框架對導航控制器進行了定制,主要添加如下功能:
> -對iOS上的interactivePopGestureRecognizer進行定制,可以方便打開或者關閉手勢交互
>
>
> -對導航控制器的Push和Pop進行功能定制,使用范圍更廣
實現UINavigationControllerDelegate的方式,對Push和Pop時的controller的變換進行跟蹤,方便后續的調試
```objective-c
- (id )navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush) {
DDLogDebug(@"navigationController will push to %@", [toVC class]);
}else if(operation == UINavigationControllerOperationPop){
DDLogDebug(@"navigationController will pop to %@", [toVC class]);
}
return nil;
}
```
## BaseWebViewController
BaseWebViewController是本框架提供出來的加載WebView的控制器,可以認為是一個帶有定制化功能的瀏覽器。它采用橋接的模式是WebViewController具有了Javascript交互的能力,具體是指我們可以再瀏覽器中執行JavaScript的語句獲取數據,同時JavaScript也可以執行注入在Controller中的OC的函數,獲取OC函數執行的數據,在Javascript運行時中使用,**簡單來說就是可以實現JS和Native的雙向數據傳輸**。具體的使用可以參考如下的部分的內容。
#橋接模塊的使用
橋接模塊的主要在iOS端實現(js -> native interface),安卓的js調用native的方式非常簡單明了,不禁想如果iOS端也有如此實現的話,這樣同時即保證安卓,iOS,h5的統一性也能讓開發者只用關心交互的接口即可。因此框架便引如了`EasyJSWebView`的第三方的框架(基于說明2設計),我們不在此過多的論述EasyJSWebView的橋接原理,只說明在框架中如何使用。
雖然在iOS7以后iOS提供了`JavaScriptCore Framework`來進行JS的控制,但是卻存在:
> - iOS端雖然也可以通過`JSContext`注入全局的方法但是達不到與安卓端統一
> - iOS端可以通過攔截h5請求的url,通過url的格式區分類或方法,但是這樣不夠直觀,也達不到與安卓端統一
因此我們在本框架中采用EasyJSWebView來實現橋接,具體步驟簡單描述如下:
-在JSInterface類中定義Native方法供JS調用
-將JSInterface類作為接口同addInterface注入到EasyJSWebView類中
-將EasyJSWebView的對象作為SubView添加到BaseWebViewController中
這樣就可以再當前BaseWebViewController加載的JS上下文環境中調用Native的方法了。
EasyJSWebView的代碼參考:https://github.com/dukeland/EasyJSWebView
# WebSocket
?WebSocket消息就是指的App跟服務端的異步通知消息,客戶端通過WebSocket協議同遠程服務器之間建立一個長連接,通過此連接,服務器可以向客戶端主動推送消息,客戶端亦可以向服務器發送請求,通過WebSocket連接,客戶端可以在請求發出后,等待服務器端發送異步處理的結果消息,避免主動查詢,減少等待時間,根據異步返回的消息來進行下一步處理。
Websocket連接建立、銷毀
異步消息通知的基礎是WebSocket連接的建立,但是由于移動客戶端的諸多限制,一直維持WebSocket連接的耗費太大,包括電量、流量等,因此websocket連接的建立和銷毀僅限以下場景(WebSocket連接的建立、銷毀對于用戶是無感知的):
-用戶登錄、登出
用戶登錄包括用戶名密碼登陸、手勢密碼登陸、一賬通登陸。在未登陸前,客戶端同服務器是沒有WebSocket連接的,因此不會有任何消息提示。在登陸后,客戶端會主動去創建WebSocket連接。相應的,用戶主動登出后,WebSocket連接被銷毀。
-應用切換
應用切換是指應用進入后臺,或者從后臺喚醒,包括鎖屏、點擊home鍵(iOS)等,當應用進入后臺時,連接被銷毀,當應用喚醒時,如果已登陸,創建連接。
-連接建立失敗處理
當WebSocket連接首次創建失敗時,客戶端會進行重試,重試的最大次數為3,每次重試之間的間隔為15秒。
本框架對第三方框架`SRWebSocket`()進行的定制化的封裝,通過QHWebSocket類初始化創建連接對象,
連接服務端
```objective-c
-(void)connect;
```
發送心跳
```objective-c
-(void)startHeartBeat;
```
發送數據
```objective-c
-(void)sendText:(NSString*)text;
-(void)sendData:(NSData*)data;
```
斷開服務器連接并銷毀
```objective-c
-(void)destroy;
```
WebSocket跟服務器的反饋,以及服務端下行通道的數據獲取通過對應代理來實現:
```objective-c
@protocol QHWebSocketDelegate
- (void)webSocket:(QHWebSocket *)webSocket didReceiveTextMessage:(NSString*)message;
- (void)webSocket:(QHWebSocket *)webSocket didReceiveBinaryMessage:(NSData*)message;
@optional
- (void)webSocketDidOpen:(QHWebSocket *)webSocket;
- (void)webSocket:(QHWebSocket *)webSocket didFailWithError:(NSError *)error;
- (void)webSocket:(QHWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
- (void)webSocket:(QHWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
- (void)applicationDidEnterBackground;
- (void)applicationWillEnterForeground;
@end
```
服務端通過WebSocket下行通知我們可以在客戶端App實現一些比較特殊的控制功能;例如:
-登陸互踢
?登陸互踢,是當用戶在一臺設備A上已經登陸,然后該賬號在另一臺設備B上又登陸了,這時,如果websocket連接保持連接狀態,服務器會向A設備發送一條登陸互踢消息,然后A設備上應用就會提示用戶賬號在其他設備登陸。

-會話超時
?會話超時,是在客戶端建立websocket連接時,服務器可能返回的消息之一,譬如當應用切換至后臺時,websocket斷開連接,然后一段時間后,重新切回應用,此時客戶端重新建立websocket連接,但是會話已超時,服務器端就會返回會話超時消息,客戶端接收到消息后就會彈出手勢密碼(如果有)重新登陸,或者跳往未登錄前首頁。
# UI組件
本框架封裝了很多iOS常用的UI組件,可以方便的修改和復用
公告彈框----AlertView
手勢密碼組件----GesturePassword
輸入框組件-----InputField(包括文字、純數字、密碼等等)
Toast組件-----MessageToast
輪播組件-----RecycleScrollView(常用于輪播廣告)
密保鍵盤------SecurityKeyboard
下拉刷新組件-----QHTableViewRefresher(列表下拉刷新的封裝)
公告等待層遮罩------WaitView
App啟動的廣告Splash-----SplashView
公共的控件工廠類-----ViewFactory(根據樣式定義公共的Button、Label、Navbar等基礎UI)
ViewFactory的部分工廠方法的示例:
```objective-c
// button類型
typedef NS_ENUM(NSUInteger, BNButtonType) {
BNButtonTypeDefault,
BNButtonTypeNavBarDarkBack,
BNButtonTypeNavBarDarkClose
};
// NavBar類型
typedef NS_ENUM(NSUInteger, BNNavigationBarType) {
BNNavigationBarTypeDefault
};
//label類型
typedef NS_ENUM(NSUInteger, BNLabelType) {
BNLabelTypeDefault,
BNLabelTypeWhite,
BNLabelTypeDark
};
+ (UIImageView *)navigationBarWithType:(BNNavigationBarType)type;
+ (UILabel *)labelWithType:(BNLabelType)type;
+ (UIButton *)buttonWithType:(BNButtonType)type;
```
# React Native支持
React Native是一種利用Javascript和React的技術來描述Native頁面布局的開發方式,我們簡要的描述一下在現有的iOS工程添加React Native支持的方式,由于該開發對H5的開發要求較高,如果你對React技術不熟悉建議先參考相關文檔。
方式一:直接創建React Native的工程
例用faceBook提供的React Native開發通過react-native init可以創建一個純使用react開發的工程,但是在實際中我們不可能完全用React來進行App的開發,因此此方式適用范圍比較窄。
關于環境的搭建請參考:
http://reactnative.cn/docs/0.40/getting-started.html
方式二:在現有的iOS工程中添加React Native的支持
1、在工程文件根目錄創建package.json
2、在使用過程中可以根據實際需要修改配置文件package.json,本文示例只添加了react和react native兩個版本的node依賴包。
```json
{
"dependencies":{
"react":"15.4.1",
"react-native": "0.42.0"
}
}
```
3、使用npm(node包管理器,Node package manager)來安裝React和React Native模塊。這些模塊會被安裝到項目根目錄下的`node_modules/`目錄中。在包含有package.json文件的目錄(一般也就是項目根目錄)中運行下列命令來安裝:
```
$ npm install
```
在安裝成功后,可以再根目錄看到node_module的文件夾,這個就是react native所有的依賴庫文件。
4、按照如下圖在項目工程中添加react native的第三方庫文件,當完成如下圖所示的工程依賴并編譯通過就表示當前的工程已經開始使用React Native進行開發了。

5、編譯工程文件,解決庫依賴文件,這樣支持React Native的文件就搭建完畢。
然后在根目錄創建React Native的入口文件index.ios.js(名稱不能改動),就可以進行相關開發了,
關于React Native的語法和文檔可以參考如下網站:
http://reactnative.cn/
http://nav.react-china.org/
https://github.com/reactnativecn/react-native-guide
index.ios.js的示例,在RCTView中用React顯示一個Label文字
```javascript
"use strict"
import React from 'react'
import {
AppRegistery,
StyleSheet,
Text,
View,
NavigatorIOS
} from 'react-native'
class RNView extends React.Component{
render() {
var param = this.props.param;
return (
Text {{param}}
)
},
var styles = StyleSheet.create({
container:{
flex:1,
justifyContent:'center',
alignItems:'center'
backgroundColor:'#FFFFFF'
}
labelText:{
fontSize:10
textAlign:'center'
color:'#333333'
}
})
}
AppRegistery.registerComponent('RNView', () => lo-view-ios);
```