Bo2SS

Bo2SS

t[i]ngshu[O]最舒適的閱讀[S]hi間是(3)分鐘?!

72th 國慶

對不起,這個系列做不到,只為給感興趣的你帶來最充實最詳細的 iOS 學習體驗,閱讀全文可能需要 3 小時,可能 30 分鐘,也可能 3 分鐘,還可能 3 秒,之後我會分析分析有多少讀者只堅持了 3 秒,繼續改進。

image

歡迎回到從《蝦票票》帶你入門 iOS 系列(3)—— 常用 UI 組件。這一篇主要聊聊iOS 的系統架構、UIKit 組件的重要成員以及它們的基本使用和特性、《蝦票票》UI 解剖

iOS 系統架構#

iOS 的系統架構主要分為 4 層,從下到上分別是核心系統層(Core OS)、核心服務層(Core Services)、媒體層(Media)以及觸摸層(Cocoa Touch),再往上就構成了一個應用(Application)。

image

從更細的粒度再看一下這 4 層的主要組成框架:

image

對於入門,其實主要關注:觸摸層的「UIKit 框架」➕核心服務層的「Foundation 框架」即可,下面的介紹來自蘋果開發者文檔:

  • UIKit:為您的 iOS 或 tvOS 應用程序構建和管理圖形化、事件驅動的用戶界面;
  • Foundation:訪問基本數據類型、集合和操作系統服務,以定義應用程序的基礎功能層。

它們的框架組成可查看附錄 1 和附錄 2~

關於 Foundation,主要先關注基本數據類型的封裝,可參閱Objective_C 基礎框架—— 易百教程;關於UIKit,它是我們今天的主角❗️

UIKit#

下面是 UIKit 重要成員的繼承關係及各自的作用:

  • UIView—— 展示內容,提供交互
    • UIImageView—— 展示圖片
    • UILabel—— 展示文字
    • UIScrollView—— 展示大於螢幕的內容(如地圖)
      • UITableView—— 展示表格類型內容(如電話簿)

UIScrollView

UITableView

  • UIViewController—— 管理 View 的容器
    • UITabBarController—— 管理多個 ViewController 切換,標籤頁切換方式
    • UINavigationController—— 管理多個 ViewController 切換,推入推出切換方式

UITabBarController

UINavigationController

對它們有一個粗略的認識後,來做一個小練習,下面兩個動圖中主要包含了哪些 UIView 和哪些 UIViewController:

UITableView、UINavigationController...

UITableView、UITabBarController、UINavigationController...

如果你能很快地判斷出來它們的基本構成,說明你已經對 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)棧結構管理子視圖:其可以添加多個子視圖,後添加的視圖展示在先添加的上面,父視圖可以管理子視圖的視圖層級。

image

從上面這張圖很容易看出兩個方塊添加的順序,先紅後綠。

⚠️:

  • 這樣的特性很可能是交互失效的原因之一,因為對於有重合的 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,與上文呼應了❗️

image

RootView 就是 UIViewController 對象自帶的一個 UIView 對象。

2)下面是它的生命週期,開發者可以選擇在合適的時機重寫並添加一些自定義操作。

  • init ->loadView->
  • viewDidLoad->viewWillAppear->viewDidAppear->
  • viewWillDisappear->viewDidDisappear->
  • dealloc

image

  • 這張圖引自 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

主要有以下這些:

image

上面代碼裡設置的 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)設置手勢

image

《蝦票票》詳情頁面裡的標籤欄(電影簡介、演員信息、更多信息)是基於 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 屬性

image

再看看《蝦票票》這張動圖,它的 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 步:

  1. 復用池裡找 kCellId (自定義的 Cell 代號)對應的 cell ,如果不存在,再手動生成相應 cell ,並在復用池裡註冊其代號為 kCellId
  2. 設置 cell 數據
  3. 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 個待實現方法的作用:

image

這份電影表格裡的電影數量是根據網絡請求的數據決定的;每個 Cell 的格式是可以復用的;行高固定;點擊某一部電影後的畫面請自行腦補。

特性#

1)UITableView 只負責展示,數據、Cell 及交互需要開發者提供

i. dataSource:數據源、Cell 內容等

ii. delegate:Cell 交互、配置等

2)復用池

因為生成和銷毀 Cell 很耗性能,所以有了復用池機制,其基本原理如下:

image

復用池的底層實現是基於雙端隊列(dequeue),向上滑動螢幕時,上面的 Cell 消失,被系統自動回收,放入復用池,下面的 Cell 需要加載,先根據其唯一標識 id 去復用池裡檢索,如果找到了,就可以成功復用,否則,再手動生成相應 Cell,並將其註冊到復用池中,綁定 id。

從下圖可以體會復用池的精髓:

image

往上滑動時,新加載的 Cell 不僅復用了 Cell 的樣式,還把右邊綠色的勾選✅也復用了。

實際使用中,這樣做是有些滑稽的,所以我們一般會在復用 Cell 後,設置其數據,上面基本使用的第 3 步有提到;或者,重寫 prepareForReuse 方法,在每次復用 Cell 前,清理不需要的數據。(注:prepareForReuse 是 UITableViewCell 的方法)

到這裡,我們再對 UITableView 的完整組成進行了解:

image

剛剛我們展示的《蝦票票》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:

image

該 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:

image

在點擊 Show 按鈕的時候,推入了另一個 Controller。

特性#

1)UINavigationBar,即頁面上方的導航欄:

引自《極客時間》

同樣是 UIViewController 對象自己去修改其頁面 UINavigationBar 的內容:

controller4.navigationItem.title = @“內容";

如標題等,同時可以自定義兩側的按鈕。

2)棧管理:可以返回上一级,也可以返回到根視圖或指定視圖。


通過上面的講述,你是否領會 UITabBarController 和 UINavigationController 的區別了呢?

下面還延伸 2 個知識點,好學生最喜歡的環節~其實也能加深對上面內容的理解。

延伸 1:常用的頁面切換方式#

從剛才的 2 種 Controller 其實可以感受到 2 種不同的頁面切換方式,那麼常用的頁面切換方式有哪些呢?

  1. tabs:您知道是指誰吧~
  2. push /pop:您知道是指誰吧~
  3. present /dismiss:與第 2 種方式不同,它一般用於不同業務界面之間的切換,並且只能逐級返回。

image

  1. show(iOS 8.0+)

該方式會根據 ViewController 的類型自動選擇切換方式,比如 splitViewController 的切換方式如下圖:

image

一般見於 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 組件組成的,直接上圖!

image

👏今天是否收穫滿滿呢?如果你可以復現上圖,那你對 UIKit 的掌握一定很不錯啦!歡迎留言說說你的看法,或者對哪裡有疑問。

下週再見#

我們會聊到 Xcode 裡的調試大法,敬請期待。

附錄#

1)Foundation

image

2)UIKit

image

通過觀察組件的父類,可以很方便地定位組件的功能。

3)MVC 模式

image

MVC 模式是一種軟件架構模式,MVC 是三個單詞的首字母縮寫,它們分別是 Model(模型)、View(視圖)和 Controller(控制):

1)Model,是程序需要操作的數據或信息。

2)View,是提供給用戶的操作界面,是程序的外殼。

3)Controller,在上面兩者中間,根據用戶在 View 中輸入的指令,選取 Model 中的數據,並進行相應的操作,產生最終結果。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。