對不起,這個系列做不到,只為給感興趣的你帶來最充實最詳細的 iOS 學習體驗,閱讀全文可能需要 3 小時,可能 30 分鐘,也可能 3 分鐘,還可能 3 秒,之後我會分析分析有多少讀者只堅持了 3 秒,繼續改進。
歡迎回到從《蝦票票》帶你入門 iOS 系列(3)—— 常用 UI 組件。這一篇主要聊聊iOS 的系統架構、UIKit 組件的重要成員以及它們的基本使用和特性、《蝦票票》UI 解剖。
iOS 系統架構#
iOS 的系統架構主要分為 4 層,從下到上分別是核心系統層(Core OS)、核心服務層(Core Services)、媒體層(Media)以及觸摸層(Cocoa Touch),再往上就構成了一個應用(Application)。
從更細的粒度再看一下這 4 層的主要組成框架:
對於入門,其實主要關注:觸摸層的「UIKit 框架」➕核心服務層的「Foundation 框架」即可,下面的介紹來自蘋果開發者文檔:
- UIKit:為您的 iOS 或 tvOS 應用程序構建和管理圖形化、事件驅動的用戶界面;
- Foundation:訪問基本數據類型、集合和操作系統服務,以定義應用程序的基礎功能層。
它們的框架組成可查看附錄 1 和附錄 2~
關於 Foundation,主要先關注基本數據類型的封裝,可參閱Objective_C 基礎框架—— 易百教程;關於UIKit,它是我們今天的主角❗️
UIKit#
下面是 UIKit 重要成員的繼承關係及各自的作用:
- UIView—— 展示內容,提供交互
- UIImageView—— 展示圖片
- UILabel—— 展示文字
- UIScrollView—— 展示大於螢幕的內容(如地圖)
- UITableView—— 展示表格類型內容(如電話簿)
- UIViewController—— 管理 View 的容器
- UITabBarController—— 管理多個 ViewController 切換,標籤頁切換方式
- UINavigationController—— 管理多個 ViewController 切換,推入推出切換方式
對它們有一個粗略的認識後,來做一個小練習,下面兩個動圖中主要包含了哪些 UIView 和哪些 UIViewController:
如果你能很快地判斷出來它們的基本構成,說明你已經對 UIKit 基礎組件的作用有一個基本的認識了~
那麼這些基礎的組件該怎麼使用,又分別有哪些特性呢?
下面我們先來聊聊其中兩位重量級成員:UIView 與 UIViewController。
UIView#
展示內容,提供交互
基本使用#
// 1)alloc:根據UIView申請空間;init:初始化對象
UIView *view = [[UIView alloc] init];
// 2)左上角點坐標(100, 100),寬高100 * 100
view.frame = CGRectMake(100, 100, 100, 100);
// +設置背景色
view.backgroundColor = [UIColor redColor]; // 等同於UIColor.redColor
// 3)將新定義的view添加到父view中
[self.view addSubview:view];
下面這 3 步是必須的。(❗️:繼承自 UIView 類型的組件大都離不開這 3 步)
1)初始化:首先通過調用 UIView 的類方法 alloc 申請空間,通過返回的實例調用 init 方法初始化對象;
2)設置 frame:即它的左上角坐標以及寬高;
3)添加:將定義好的 UIView 對象添加到一個父 UIView 對象上。
特性#
1)棧結構管理子視圖:其可以添加多個子視圖,後添加的視圖展示在先添加的上面,父視圖可以管理子視圖的視圖層級。
從上面這張圖很容易看出兩個方塊添加的順序,先紅後綠。
⚠️:
- 這樣的特性很可能是交互失效的原因之一,因為對於有重合的 View,上層的交互會讓下層的交互失效。
- 細心的朋友可能會疑惑代碼中的self是個怎樣的存在,它的 view 屬性,這就是下面要提到的第 2 個重量級成員。
OC 作為一門面向對象編程語言,self 指代的其實就是當前調用該方法的對象,而上面 self 的類型就是 UIViewController。
UIViewController#
管理 View 的容器
如果了解 MVC 模式(見文末附錄 3),其與 UIView 的關係就很容易理解了:UIView 是 MVC 中的 V--View,UIViewController 是 MVC 中的 C--Controller。
基本使用#
// 1)alloc:根據UIViewController申請空間;init:初始化對象
UIViewController *viewController = [[UIViewController alloc] init];
// 2)將某一個view附到viewController的view中
[viewController.view addSubview:someView];
1)初始化
2)添加要管理的 UIView 對象
特性#
1)相當於一個容器,自身包含一個默認的 view 屬性,類型為 UIView,與上文呼應了❗️
RootView 就是 UIViewController 對象自帶的一個 UIView 對象。
2)下面是它的生命週期,開發者可以選擇在合適的時機重寫並添加一些自定義操作。
- init ->loadView->
- viewDidLoad->viewWillAppear->viewDidAppear->
- viewWillDisappear->viewDidDisappear->
- dealloc
- 這張圖引自 quanqingyang 的 CSDN 博客,已經很好地說明了 UIViewController 對象從初始化👉加載視圖👉展示視圖👉視圖消失👉卸載視圖👉銷毀的過程。
- 初學者一般關注 init 和 viewDidLoad,在 init 時添加一些自定義對象的初始化,在 viewDidLoad 時給 self.view 添加一些自定義的子視圖。
PS:
- 新建一個工程時,會自動生成一個 ViewController(.h 頭文件和.m 源文件),其默認作為整個工程的根 ViewController。
- 一般地,我們會封裝一些繼承自 UIViewController 的自定義 ViewController,在裡面添加特定的視圖和交互邏輯。
聊完了 2 位重量級成員,可以稍微休息一會兒~接下來看看它們後代們的基本使用和特點。
UIView 的子孫們#
UIImageView#
展示圖片
基本使用#
// 1)alloc:根據UIImageView申請空間;init:初始化對象
UIImageView *imageView = [[UIImageView alloc] init];
// 2)左上角點坐標(100, 100),寬高100 * 100
imageView.frame = CGRectMake(100, 100, 100, 100);
// 3)設置圖片的填充方式
imageView.contentMode = UIViewContentModeScaleAspectFill;
// 4)將指定圖片定義成UIImage對象,賦給imageView的image屬性
#define kImageName @"./ford.jpg"
imageView.image = [UIImage imageNamed:kImageName];
1)初始化
2)設置 frame
3)設置圖片的填充方式 contentMode
主要有以下這些:
上面代碼裡設置的 contentMode 為 UIViewContentModeScaleAspectFill,對應上圖第二個示例,這是比較常用的方式。
4)給 image 屬性賦值
所有的圖片都要先封裝成 UIImage 對象,再通過 UIImageView 展示。
特性#
1)展示動態圖片:給 UIImageView 的 animationImages 屬性傳 UIImage 類型的數組;
2)相關第三方庫:SDWebImage,它可以優化 UIImage 的生成過程,一般用於拉取網絡圖片。
PS:SDWebImage:https://github.com/SDWebImage/SDWebImage
UILabel#
展示文字
基本使用#
// 1)初始化
UILabel *summaryTabLabel = [[UILabel alloc] init];
// 2)左上角點坐標(100, 100),寬高100 * 100
imageView.frame = CGRectMake(100, 100, 100, 100);
// 3)設置文本
summaryTabLabel.font = [UIFont systemFontOfSize:14];
summaryTabLabel.textAlignment = NSTextAlignmentCenter;
summaryTabLabel.textColor = [UIColor orangeColor];
[summaryTabLabel setText:@“電影簡介"];
// PS:設置手勢
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(summaryTabTapAction)];
[summaryTabLabel addGestureRecognizer:tapGesture];
summaryTabLabel.userInteractionEnabled = YES;
1)初始化
2)設置 frame
3)設置文本:字體、對齊方式、文本顏色、文本內容
PS)設置手勢
《蝦票票》詳情頁面裡的標籤欄(電影簡介、演員信息、更多信息)是基於 UILabel 實現的,給它們添加點擊的手勢,便可以實現像按鈕一樣的點擊效果。
⚠️:UILabel 對象的交互默認是關閉的,需要設置 userInteractionEnabled 屬性為 YES 來開啟。
特性#
1)多行顯示(如上面動圖中每個標籤欄裡的內容)
summaryTabLabel.numberOfLines = 0; // 默認為1
[summaryTabLabel sizeToFit];
2)文本截斷方式:lineBreakMode 屬性,如按字符換行,省略中間部分顯示等等
3)展示複雜的文本:使用 NSAttributeString 類型、YYText(第三方庫)
UIScrollView#
展示大於螢幕的內容
基本使用#
// 1)初始化並設置可視範圍——frame
UIScrollView *detailScrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
// 2)設定滾動範圍——contentSize
#define kScreenWidth UIScreen.mainScreen.bounds.size.width
#define kScreenHeight UIScreen.mainScreen.bounds.size.height
detailScrollView.contentSize = CGSizeMake(kScreenWidth * 3, kScreenHeight);
// PS:是否分頁滾動
detailScrollView.pagingEnabled = YES;
1)初始化並設置 frame
frame 是該視圖的可視範圍,在初始化的時候就可以設置,使用 initWithFrame 而不是 init 了。
上面代碼中賦值的 self.view.frame 就是 self.view 的 frame,self.view 你應該知道是什麼了吧,一開始提到過的。
2)設定 contentSize
contentSize 是該視圖的滾動範圍,換句話說是它的全部範圍。
PS)設置滾動是否以頁為單位:pagingEnabled 屬性
再看看《蝦票票》這張動圖,它的 contentSize 是三倍螢寬,分頁滾動。
特性#
1)frame 和 contentSize
前者是可視範圍,後者是滾動範圍。
它倆是 UIScrollView 初始化時最重要的兩個屬性,設置不對可能會滾不起來哦~
2)setContentOffset 方法
[detailScrollView setContentOffset:CGPointMake(detailScrollView.bounds.size.width * 2, 0) animated:YES];
在上一小節裡,點擊標籤欄觸發 UIScrollView 的滾動,即 UILabel 點擊手勢對應的事件 summaryTabTapAction 裡,就需要用到 setContentOffset 方法。
下面是 UIView 的子孫裡輩分最小的一位,它是繼承自 UIScrollView 的,同時也是這裡最難理解的。
UITableView : UIScrollView#
展示表格類型
基本使用#
// 1)初始化並設置可視範圍——frame
UITableView *homeTableView = [[UITableView alloc] initWithFrame:self.view.bounds];
// 2)指定dataSource、delegate代理方(記得在類聲明時添加兩個協議)
homeTableView.dataSource = self;
homeTableView.delegate = self;
// 3)接下來實現協議裡的方法(@required必須項、@optional可選項)
1)初始化並設置 frame
frame 指可視範圍,類同 UIScrollView。
2)指定 dataSource 和 delegate 的代理方
dataSource 和 delegate 是 UITableView 的 2 個協議,「指定代理方」的意思是讓「他人」來遵守這個協議,幫忙做協議裡的一些事情,這些事情有些是必須做的,有些是可選的;可以類比我們生活中的勞動協議、租賃合同等等。
i. dataSource 負責表格的數據源、單元內容等;
ii. delegate 負責單元的交互、配置等。
這裡還需要明確一下,上面代碼是寫在Controller裡的,self 指的是 UIViewController 類型的對象。也就是說,self 遵守了這兩個協議,並需要去實現協議裡的方法。
3)實現協議裡的方法
i. dataSource
#pragma mark - UITableViewDataSource
// @required
// 1)設置每個Section緩衝的Cell數量
(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return 3;
}
// 2)渲染每個Cell的內容
(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 1.從復用池裡找kCellId對應的cell(#define kCellId@“id1”)
// [tableView dequeueReusableCellWithIdentifier:kCellId];
// 1-2.如果不存在,再手動生成相應cell並註冊為kCellId
// [… reuseIdentifier:kCellId];
// 2.設置cell數據
// 3.return cell
}
// @optional…
dataSource 協議裡有 2 個 @required
(必須實現)和若干個 @optional
(可選實現)的方法。
第 1 個 @required
的方法,用來設置每個 Section(分區)緩衝的 Cell(單元)數量, return 3
代表只渲染 3 條數據。實際使用時,一般會根據網絡請求返回的數據個數來決定返回。
第 2 個 @required
的方法,用來設置每個單元渲染的內容,參考代碼註釋,一般分為 3 步:
- 從復用池裡找
kCellId
(自定義的 Cell 代號)對應的cell
,如果不存在,再手動生成相應cell
,並在復用池裡註冊其代號為kCellId
- 設置
cell
數據 return cell
PS:關於復用池一會兒會再次提到,我們先和它混個面熟。UITableViewCell 類型繼承自 UIView,其基本使用和特性大同小異。
ii. delegate
#pragma mark - UITableViewDelegate
// @optional
// 1)設置每個Cell的行高
(CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 100;
// 2)設置點擊Cell後的事件
(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// do Something after cell selected
}
// …
delegate 協議裡有若干個 @optional
(可選實現)的方法,上面列出了 2 個常用的待實現方法。
第 1 個 @optional
的方法,用來設置每個 Cell 的行高。
第 2 個 @optional
的方法, 用來設置點擊 Cell 後的事件,比如推入另一個頁面。
根據《蝦票票》的首頁再感受一下上述 4 個待實現方法的作用:
這份電影表格裡的電影數量是根據網絡請求的數據決定的;每個 Cell 的格式是可以復用的;行高固定;點擊某一部電影後的畫面請自行腦補。
特性#
1)UITableView 只負責展示,數據、Cell 及交互需要開發者提供
i. dataSource:數據源、Cell 內容等
ii. delegate:Cell 交互、配置等
2)復用池
因為生成和銷毀 Cell 很耗性能,所以有了復用池機制,其基本原理如下:
復用池的底層實現是基於雙端隊列(dequeue),向上滑動螢幕時,上面的 Cell 消失,被系統自動回收,放入復用池,下面的 Cell 需要加載,先根據其唯一標識 id 去復用池裡檢索,如果找到了,就可以成功復用,否則,再手動生成相應 Cell,並將其註冊到復用池中,綁定 id。
從下圖可以體會復用池的精髓:
往上滑動時,新加載的 Cell 不僅復用了 Cell 的樣式,還把右邊綠色的勾選✅也復用了。
實際使用中,這樣做是有些滑稽的,所以我們一般會在復用 Cell 後,設置其數據,上面基本使用的第 3 步有提到;或者,重寫 prepareForReuse
方法,在每次復用 Cell 前,清理不需要的數據。(注:prepareForReuse
是 UITableViewCell 的方法)
到這裡,我們再對 UITableView 的完整組成進行了解:
剛剛我們展示的《蝦票票》Demo 僅僅是一個分區,也沒有設置 Header 和 Footer。
實際上,一個完整的 UITableView 可參考上圖,用公式表示如下:
UITableView = tableHeaderView + n ✖️ Section + tableFooterView
其中,Section = sectionHeader + n ✖️ UITableViewCell + sectionFooter
終於終於,你是否堅持到這裡了呢?「UIView 的子孫們」打卡點,💳滴~
下面是 UIViewController 的 2 個爭氣的孩子。
UIViewController的孩子們#
這 2 位小孩的相同點是:都可以管理多個 UIViewController 對象的切換,聽起來像是兒子也可以管理多個爸爸~😛
不同點可以從下面介紹中對照著體會:
UITabBarController#
基本使用#
// 1)初始化
UITabBarController *tabbarController = [[UITabBarController alloc] init];
// 2)設置要管理的多個Controller(屬於或繼承自UIViewController)
[tabbarController setViewControllers:@[controller1, controller2, controller3]];
1)初始化
2)設置要管理的多個 Controller
在一開始就設置好了,每個 Controller 的類型屬於或繼承自 UIViewController。
再看看它的 Demo:
該 UITabBarController 對象管理了 3 個 Controller。
特性#
1)UITabBar,即頁面下方的切換欄:
被管理的 UIViewController 對象自己去修改UITabBar 上對應 Tab 的內容:
controller1.tabBarItem.title = @“新聞";
controller1.tabBarItem.image = [UIImage imageNamed:@"tab1.png"];
如圖標、標題等。
UINavigationController#
基本使用#
// 1)初始化,並設置根Controller
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller1];
// 2)在合適的時候push另一個Controller
[navigationController pushViewController:controller4 animated:YES];
1)初始化,並設置根 Controller
只需要設置 1 個。
2)之後在合適的時候推入另一個 Controller
如在《蝦票票》中,點擊首頁的某一部電影,推入對應的詳情界面 Controller。
再看看它的 Demo:
在點擊 Show 按鈕的時候,推入了另一個 Controller。
特性#
1)UINavigationBar,即頁面上方的導航欄:
同樣是 UIViewController 對象自己去修改其頁面 UINavigationBar 的內容:
controller4.navigationItem.title = @“內容";
如標題等,同時可以自定義兩側的按鈕。
2)棧管理:可以返回上一级,也可以返回到根視圖或指定視圖。
通過上面的講述,你是否領會 UITabBarController 和 UINavigationController 的區別了呢?
下面還延伸 2 個知識點,好學生最喜歡的環節~其實也能加深對上面內容的理解。
延伸 1:常用的頁面切換方式#
從剛才的 2 種 Controller 其實可以感受到 2 種不同的頁面切換方式,那麼常用的頁面切換方式有哪些呢?
- tabs:您知道是指誰吧~
- push /pop:您知道是指誰吧~
- present /dismiss:與第 2 種方式不同,它一般用於不同業務界面之間的切換,並且只能逐級返回。
- show(iOS 8.0+)
該方式會根據 ViewController 的類型自動選擇切換方式,比如 splitViewController 的切換方式如下圖:
一般見於 iPad 設備,如淘寶 App。
延伸 2:兩種嵌套方式#
在實際使用中,UITabBarController 和 UINavigationController 經常是結合起來使用的,與其說是結合方式,不如說是嵌套方式。
方式一:
方式二:
這兩種方式最明顯的區別在於頁面切換時,TabBar 是否會消失。
但是為什麼 Apple 官方推薦第一種方式呢?歸納起來大概有 2 點:
1)自由度更高,每個 NavigationController 是獨立的,並且可以選擇不嵌套 NavigationController;
2)可以實現與方式二相同的頁面切換效果,手動隱藏 Tab 欄即可。
注:UI 繪製都在主線程#
今天大費篇幅介紹了 UIKit 的重要組件們,那麼在使用它們的過程中一定不要忘了最後這一點提示:UI 繪製都是在主線程❗️
至於為什麼?這裡摘抄了為什麼必須在主線程操作 UI—— 掘金這篇文章裡的內容。
1)UIKit 是一個線程不安全的類。UI 操作涉及到訪問各種 View 對象的屬性,如果異步操作,會存在讀寫問題;如果為其加鎖,又會耗費大量資源並拖慢運行速度;
2)只能在主線程上才能對事件進行響應。整個程序的起點 UIApplication 是在主線程進行初始化,所有的用戶事件都是在主線程上進行傳遞(如點擊、拖動),所以 UI 只能在主線程上才能對事件進行響應;
3)確保實現圖像的同步更新。在渲染方面,由於圖像的渲染需要以 60 幀的刷新率在螢幕上同時更新,而在非主線程異步化的情況下,無法確定這個處理過程能否實現同步更新。
所以,在進行 UI 繪製時,一定要關注當前線程是否為主線程!
《蝦票票》UI 解剖#
下面趁熱打鐵,來看看《蝦票票》是由哪些 UIKit 組件組成的,直接上圖!
👏今天是否收穫滿滿呢?如果你可以復現上圖,那你對 UIKit 的掌握一定很不錯啦!歡迎留言說說你的看法,或者對哪裡有疑問。
下週再見#
我們會聊到 Xcode 裡的調試大法,敬請期待。
附錄#
1)Foundation
2)UIKit
通過觀察組件的父類,可以很方便地定位組件的功能。
3)MVC 模式
MVC 模式是一種軟件架構模式,MVC 是三個單詞的首字母縮寫,它們分別是 Model(模型)、View(視圖)和 Controller(控制):
1)Model,是程序需要操作的數據或信息。
2)View,是提供給用戶的操作界面,是程序的外殼。
3)Controller,在上面兩者中間,根據用戶在 View 中輸入的指令,選取 Model 中的數據,並進行相應的操作,產生最終結果。