天天看點

第十四章——UINavigationController

在第五章中,您了解了 UITabBarController 以及它如何允許使用者通路不同的螢幕。 标簽欄控制器非常适合彼此獨立的螢幕,但如果您有多個提供相關資訊的螢幕怎麼辦?

例如,

Settings

應用程式具有多個相關資訊螢幕:一個設定清單(如

Sounds

),每個設定的詳細頁面以及每個詳細資訊的選擇頁面(圖14.1)。 這種類型的界面稱為 向下鑽取界面(

drill-down interface

)。

圖14.1“Settings” 中的向下鑽取界面

第十四章——UINavigationController

在本章中,您将使用 UINavigationController 向

Homepwner

添加一個 向下鑽取界面,該界面允許使用者檢視和編輯 Item 的詳細資訊。 這些詳細資訊将由您在第十三章(圖14.2)中建立的 DetailViewController 顯示。

圖14.2 使用 UINavigationController 的 Homepwner

第十四章——UINavigationController

UINavigationController

UINavigationController 包含一個視圖控制器數組,用于在棧中呈現相關資訊。 當 UIViewController 位于棧頂部時,其視圖是可見的。

你将使用 UIViewController 初始化一個 UINavigationController 的執行個體。 該 UIViewController 被添加到導航控制器的

viewControllers

數組,并成為導航控制器的根視圖控制器。 根視圖控制器始終位于棧底。 (請注意,盡管該視圖控制器被稱為導航控制器的 “根視圖控制器”,然而 UINavigationController 沒有

rootViewController

這個屬性。)

更多的視圖控制器可以在應用程式運作時被 push 到 UINavigationController 的棧頂。 這些視圖控制器被添加到對應于棧頂的

viewControllers

數組的末尾。

UINavigationController 的

topViewController

屬性保留對堆棧頂部的視圖控制器的引用。

當一個視圖控制器 壓棧(push) 時,它的視圖就會從右邊移到螢幕上。當 彈棧(pop) 時(例:最後一項被移除時),頂部視圖控制器從棧中移開,它的視圖滑到右邊,将下一個視圖控制器的視圖暴露在棧上并将成為頂部視圖控制器。圖14.3顯示了一個帶有兩個視圖控制器的導航控制器。

topViewController

的視圖是使用者所看到的。

圖14.3 UINavigationController 的棧

第十四章——UINavigationController

UINavigationController 是 UIViewController 的子類,是以它具有自己的視圖。它的視圖總是有兩個子視圖:一個 UINavigationBar 和

topViewController

的視圖(圖14.4)。

圖14.4 UINavigationController 的視圖

第十四章——UINavigationController

在本章中,您将向

Homepwner

應用程式添加一個 UINavigationController,并使 ItemsViewController 成為 UINavigationController 的根視圖控制器。 當選擇 Item 時, DetailViewController 将被 push 到 UINavigationController 的棧上。該視圖控制器将允許使用者檢視和編輯在 ItemsViewController 的表視圖中選中的 Item 的屬性。 更新的

Homepwner

應用程式的對象圖如圖14.5所示。

圖14.5 Homepwner 對象圖

第十四章——UINavigationController

這個應用程式變得相當大,你可以看到。 幸運的是,視圖控制器和 UINavigationController 知道如何處理這種複雜的對象圖。 在編寫iOS應用程式時,将每個 UIViewController 視為自己的小世界很重要。 在 Cocoa Touch 中已經實施的内容将會起到舉足輕重的作用。

首先重新打開

Homepwner

項目,給

Homepwner

添加一個導航控制器。 使用 UINavigationController 的唯一要求是您給它一個根視圖控制器,并将其視圖添加到視窗。

打開

Main.storyboard

并選擇

Items View Controller

。 然後,從

Editor

菜單中選擇

Embed In

Navigation Controller

。 這将将 ItemsViewController 設定為 UINavigationController 的根視圖控制器。 它還将更新故事闆,将

Navigation Controller

設定為初始視圖控制器。

您的

Detail View Controller

界面現在可能會放錯位置,因為它被包含在導航控制器中。 如果是,請選擇棧視圖,然後單擊自動布局限制菜單中的

Update Frames

按鈕。

建構并運作應用程式,然後應用程式崩潰了。 發生了什麼? 您之前與 AppDelegate

建立 關聯(

contract

)的是 ItemsViewController 的執行個體,這将會是視窗的

rootViewController

:

let itemsController = window!.rootViewController as! ItemsViewController

您現在通過将 ItemsViewController 嵌入到 UINavigationController 中來破壞此關聯。 您需要更新此關聯。

打開

AppDelegate.swift

并更新 application(_:didFinishLaunchingWithOptions :) 以反映新的視圖控制器層級。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {

  

// Override point for customization after application launch.

  

// Create an ItemStore

  

let itemStore = ItemStore()

  

// Access the ItemsViewController and set its item store

  

let itemsController = window!.rootViewController as! ItemsViewController

  

let navController = window!.rootViewController as! UINavigationController

  

let itemsController = navController.topViewController as! ItemsViewController

  

itemsController.itemStore = itemStore

  

return true

}

再次建構并運作應用程式。

Homepwner

再次正常運作,在螢幕頂部有一個非常好看的(雖然是空的) UINavigationBar (圖14.6)。

圖14.6具有空導航欄的Homepwner

第十四章——UINavigationController

請注意螢幕是如何調整來适應 ItemsViewController 的視圖以及新的導航欄的。UINavigationController 為您做了這樣的事情:雖然 ItemsViewController 的視圖實際上是在導航欄上進行的,但 UINavigationController 在頂部添加了

padding

,使一切都很适合。這是因為視圖控制器的頂部布局指南被調整了,以及所有視圖都被限制在頂部的布局指南中——就像棧視圖一樣。

UINavigationController 導航

在應用程式仍在運作的情況下,建立一個新 item 并從 UITableView 中選擇該行。你不僅被帶到 DetailViewController 的視圖中,而且你還可以在 UINavigationBar 中獲得一個自帶的動畫和一個

Back

按鈕。點選這個按鈕,你将回到 ItemsViewController。

注意,你不需要改變你在第13章建立的 show segue 來得到這個行為。正如在那一章中所提到的,show segue 以一種有意義的方式呈現了目标視圖控制器。當一個 show segue 從嵌入在導航控制器中的視圖控制器觸發時,目标視圖控制器被 push 到導航控制器的視圖控制器棧中。

因為 UINavigationController 的棧是一個數組,它将擁有對添加到它的任何視圖控制器的所有權。是以,DetailViewController 隻在 segue 完成之後由 UINavigationController 擁有。當棧被彈出時, DetailViewController 會被銷毀。下一次當一行被點選時,會建立一個 DetailViewController 的新執行個體。

使用一個視圖控制器來

push

下一個視圖控制器是一個常見的模式。根視圖控制器通常建立下一個視圖控制器,下一個視圖控制器建立後一個視圖控制器,等等。有些應用程式可能可以根據使用者輸入來 push 不同的視圖控制器。例如,根據選擇的媒體類型,

Photos

應用程式将視訊視圖控制器或圖像視圖控制器 push 到導航棧上。

請注意,item 的詳細資訊視圖包含所選 Item 的資訊。然而,當您可以編輯這些資料時, UITableView 在傳回時不會反映這些更改。為了解決這個問題,您需要實作代碼來更新正在編輯的 Item 的屬性。在下一節中,您将看到何時執行此操作。

顯示和消失的視圖

每當一個 UINavigationController 即将交換視圖時,它會調用兩個方法: viewWillDisappear(_ :) 和 viewWillAppear(_ :)。 當 viewWillDisappear(_ :) 被調用,UIViewController 将從棧彈出。 然後,當 viewWillAppear(_ :) 被調用,UIViewController 将處在該棧頂。

為了保持資料更改,當 DetailViewController 從棧中彈出時,您将将其 item 的屬性設定為文本字段的内容。 在實作這些視圖顯示和消失的方法時,重要的是調用超類的實作——它可能要做一些事,或者需要機會去做一些事。 在

DetailViewController.swift

中,實作 viewWillDisappear(_ :) 。

override func viewWillDisappear(_ animated: Bool) {

  

super.viewWillDisappear(animated)

  

// "Save" changes to item

  

item.name = nameField.text ?? ""

  

item.serialNumber = serialNumberField.text

  

if let valueText = valueField.text, let value = numberFormatter.number(from: valueText) {

    

item.valueInDollars = value.intValue

  

} else {

    

item.valueInDollars = 0

  

}

}

現在當使用者點選 UINavigationBar 上的

back

按鈕時,Item 的值将被更新。 當 ItemsViewController 出現在螢幕上時,方法 viewWillAppear(_ :) 被調用。 借此機會重新加載 UITableView ,以便使用者立即看到更改。

ItemsViewController.swift

中,重寫 viewWillAppear(_ :) 來重新加載表視圖。

override func viewWillAppear(_ animated: Bool) {

  

super.viewWillAppear(animated)

  

tableView.reloadData()

}

再次建構并運作應用程式。 現在,您可以在建立的視圖控制器之間來回移動,并輕松更改資料。

取消鍵盤

運作應用程式,添加并選擇一個 item,然後觸摸 item 的 Name 文本字段。 當您觸摸文本字段時,螢幕上會顯示一個鍵盤(圖14.7),如您在第4章中的

WorldTrotter

應用程式中所看到的。(如果您使用的是模拟器,鍵盤沒有出現,請記住可以按

Command-K

切換裝置鍵盤。)

圖14.7 觸摸文本字段時出現鍵盤

第十四章——UINavigationController

UITextField 類以及 UITextView 内置了鍵盤響應觸摸的外觀,是以您無需為鍵盤出現做任何額外的操作。但是,有時您會希望確定鍵盤的行為符合您的要求。

舉個例子,注意到鍵盤覆寫了螢幕的三分之一以上。 現在,它并沒有遮住任何東西,但是很快你會添加更多的詳細資訊,擴充到螢幕的底部,當使用者不需要鍵盤時需要一種方法來隐藏它。 在本節中,您将給使用者兩種方法來關閉鍵盤:按下鍵盤的 Return 鍵,或者點選詳情視圖控制器視圖上的其他任何位置。 但首先,我們來看看使文本可編輯的事件組合。

事件處理基礎知識

觸擊視圖時,會建立一個事件。 此事件(稱為“觸摸事件”)與視圖控制器視圖中的特定位置相關聯。 該位置确定觸摸事件将傳遞到層級中的哪個視圖。

例如,當您在其邊界内點選一個 UIButton 時,它會收到觸摸事件并以按鈕的形式進行響應——通過在其目标上調用動作方法。 當您的應用程式中的視圖被觸摸時,該視圖會接收到觸摸事件,并且可以選擇對該事件做出反應或忽略它。 但是,您的應用程式中的視圖也可以響應非觸摸事件。 一個很好的例子是搖一搖。 如果您在運作應用程式時晃動裝置,您的其中一個視圖就可以響應。 但是是哪一個呢? 另一個有趣的情況是響應鍵盤。 DetailViewController 的視圖包含三個 UITextFields。 使用者輸入時哪個會接收到文本?

對于震動和鍵盤事件,在視圖層級中沒有事件位置來确定哪個視圖将接收該事件,是以必須使用另一個機制。這個機制是 第一響應者(

first responder

) 狀态。許多視圖和控件可以是視圖層級中的第一響應者,但一次隻能有一個響應者。可以把它看作可以在視圖中傳遞的标志。無論哪個視圖持有該标志,都将接收震動或鍵盤事件。

UITextField 和 UITextView 的執行個體對觸摸事件有一個不尋常的響應。 觸摸時,文本字段或文本視圖将成為第一響應者,反而會觸發系統将鍵盤顯示在螢幕上,并将鍵盤事件發送到文本字段或視圖。 鍵盤和文本字段或視圖沒有直接的連接配接,但它們通過第一響應者狀态協同工作。

這是確定将鍵盤輸入傳遞到正确文本字段的整潔方法。 第一響應者的概念隻是 Cocoa Touch 程式設計中包含 UIResponder 類和 響應者鍊(

responder chain

) 的更廣泛的事件處理主題的一部分。 當您處理第18章中的觸摸事件時,您将了解更多資訊,您還可以通路Apple的 `Event Handling Guide for iOS* 了解更多資訊。

按Return鍵退出

現在讓我們回到允許使用者關閉鍵盤。 如果您觸摸應用程式中的另一個文本字段,則該文本字段将成為第一個響應者,鍵盤将保留在螢幕上。 當沒有文本字段(或文本視圖)是第一個響應者時,鍵盤将被放棄并離開。 要關閉鍵盤,那麼您需要在第一個響應者的文本字段上調用 resignFirstResponder()。

要使文本字段響應于按下Return鍵,您将要實作 UITextFieldDelegate 方法 textFieldShouldReturn(_ :)。 隻要按下Return鍵,就會調用此方法。

首先,在

DetailViewController.swift

中,使 DetailViewController 符合 UITextFieldDelegate 協定。

class DetailViewController: UIViewController,

UITextFieldDelegate {

接下來,在傳入的文本字段上實作 textFieldShouldReturn(_ :) 來調用 resignFirstResponder()。

func textFieldShouldReturn(_ textField: UITextField) -> Bool {

  

textField.resignFirstResponder()

  

return true

}

最後,打開

Main.storyboard

并将每個文本字段的

delegate

屬性連接配接到

Detail View Controller

(圖14.8)。(右鍵從每個 UITextField 拖動到

Detail View Controller

并選擇

delegate

。)

圖14.8 連接配接文本字段的委托屬性

第十四章——UINavigationController

建構并運作應用程式。 點選文本字段,然後按鍵盤上的 Return 鍵。 鍵盤将消失。 要使鍵盤傳回,請觸摸任何文本字段。

點選其他地方傳回

如果使用者在 DetailViewController 的視圖上輕觸其他任何地方,也應該會關閉鍵盤。 為了做到這一點,當視圖被觸摸時,您将使用手勢識别器,就像在

WorldTrotter

應用程式中一樣。 在動作方法中,您将在文本字段上調用 resignFirstResponder() 。

打開

Main.storyboard

并在對象庫中找到

Tap Gesture Recognizer

。 将此對象拖動到

Detail View Controller

的背景視圖上。 您将在 scee 底部中看到此手勢識别器的引用。

在項目導航器中,右擊 DetailViewController.swift 在

assistant editor

中打開它。 右鍵從故事闆中的 tap gesture recognizer 拖動到 DetailViewController 的實作類。

在出現的彈出視窗中,從

Connection

菜單中選擇

Action

。 命名動作 backgroundTapped 。 對于

Type

,選擇

UITapGestureRecognizer

(圖14.9)。

圖14.9 配置UITapGestureRecognizer操作

第十四章——UINavigationController

單擊

Connect

,動作方法的存根将顯示在

DetailViewController.swift

中。 在 DetailViewController 的視圖上更新調用 endEditing(_ :) 的方法。

@IBAction func backgroundTapped(_ sender: UITapGestureRecognizer) {

  

view.endEditing(true)

}

調用 endEditing(_ :) 是一種友善的方式來解除鍵盤,你不必知道(或關心)哪個文本字段是第一個響應者。 當視圖獲得此調用時,它将檢查其層級中的任何文本字段是否是第一個響應者。 然後會在該特定的視圖上調用 resignFirstResponder()。

建構并運作您的應用程式。 點選文本字段以顯示鍵盤。 點選文本字段外的視圖,鍵盤也将消失。

最後一個需要關掉鍵盤的情況。 當使用者點選後退按鈕時,在将其從彈出的堆棧之前的 DetailViewController 上調用 viewWillDisappear(_ :),并且鍵盤立即消失,沒有動畫。 要更順利地關閉鍵盤,請在

DetailViewController.swift

中更新 viewWillDisappear(_ :) 的實作,以調用 endEditing(_ :) 。

override func viewWillDisappear(_ animated: Bool) {

  

super.viewWillDisappear(animated)

  

// Clear first responder

  

view.endEditing(true)

  

// "Save" changes to item

  

item.name = nameField.text ?? ""

  

item.serialNumber = serialNumberField.text

  

if let valueText = valueField.text, let value = numberFormatter.number(from: valueText) {

    

item.valueInDollars = value.integerValue

  

} else {

    

item.valueInDollars = 0

  

}

}

UINavigationBar

在本節中,您将聲明 UINavigationBar, 一個 UIViewController 的描述性标題,并正好位于 UINavigationController 的棧頂。

每個 UIViewController 都有一個類型為 UINavigationItem 的

navigationItem

屬性。 但是,與 UINavigationBar 不同的是,UINavigationItem 不是 UIView 的子類,是以它不能出現在螢幕上。 相反,導航項目為導航欄提供了需要繪制的内容。 當一個 UIViewController 到達 UINavigationController 的棧頂時,UINavigationBar 使用 UIViewController 的

navigationItem

進行配置,如圖14.10所示。

圖14.10 UINavigationItem

第十四章——UINavigationController

預設情況下,UINavigationItem 為空。 在最基本的層次上, UINavigationItem 有一個簡單的

title

字元串。 當 UIViewController 移動到導航棧頂,并且其

navigationItem

具有有效字元串的

title

屬性時,導航欄将顯示該字元串(圖14.11)。

圖14.11 帶有标題的UINavigationItem

第十四章——UINavigationController

ItemsViewController 的标題将始終保持不變,是以您可以在故事闆本身中設定其導航項的标題。

打開

Main.storyboard

。 輕按兩下

Items View Controller

上方導航欄的中央以編輯其标題。 給它一個标題 “Homepwner”(圖14.12)。

圖14.12 在故事闆中設定标題

第十四章——UINavigationController

建構并運作應用程式。 注意導航欄上的字元串

Homepwner

。 建立并點按一行,并注意導航欄不再具有标題。 将 DetailViewController 的導航項标題作為它正在顯示的 Item 的名稱是很好的。 因為标題将取決于正在顯示的 Item,您需要在代碼中動态設定

navigationItem

的标題。

DetailViewController.swift

中,将屬性觀察器添加到更新

navigationItem

标題的

item

屬性中。

var item: Item!

{

  

didSet {

    

navigationItem.title = item.name

  

}

}

  

建構并運作應用程式。 建立并點按一行,您将看到導航欄的标題是您選擇的 Item 的名稱。

導航項可以儲存多個标題字元串,如圖14.13所示。 每個 UINavigationItem 有三個可自定義的區域:一個

leftBarButtonItem

,一個

rightBarButtonItem

和一個

titleView

。 左和右欄的按鈕項是對 UIBarButtonItem 的執行個體的引用,它包含僅可以在 UINavigationBar 或 UIToolbar 上顯示的按鈕的資訊。

圖14.13 UINavigationItem

第十四章——UINavigationController

回想一下,UINavigationItem 不是 UIView 的子類。 相反, UINavigationItem 封裝了 UINavigationBar 用于配置自身的資訊。 類似地,UIBarButtonItem 不是視圖,而是儲存有關如何顯示 UINavigationBar 上單個按鈕的資訊。( UIToolbar 還使用 UIBarButtonItem 的執行個體配置自身。)

UINavigationItem 的第三個可定制區域是它的

titleView

。 您可以使用基本字元串作為标題,也可以将 UIView 的子類置于導航項目的中心。 你不能同時擁有這兩個。 如果它适合特定視圖控制器的上下文以具有自定義視圖(例如分段控件或文本字段),則可以将導航項的

titleView

設定為該自定義視圖。 圖14.13顯示了具有自定義視圖作為其

titleView

的 UINavigationItem 的内置

Maps

應用程式的示例。 然而,通常,标題字元串就足夠了。

将按鈕添加到導航欄

在本節中,當

ItemsViewController

位于棧頂時,您将使用兩個按鈕項替換表頭視圖中的兩個按鈕,這些按鈕項将出現在 UINavigationBar 中。 導覽列按鈕項(

Bar Button Item

)具有像 UIControl 的目标動作機制一樣的目标動作對:當點選時,它将該動作消息發送到目标。

首先,我們來處理一個添加新 item 的按鈕項。 當 ItemsViewController 位于棧頂時,此按鈕将位于導航欄的右側。 點選後,它将添加一個新的 Item。

在更新故事闆之前,需要更改 addNewItem(_ :) 的方法聲明。 目前這種方法是由 UIButton 觸發的。 現在您正在将發件人更改為 UIBarButtonItem,您需要更新聲明。

ItemsViewController.swift

中,更新 addNewItem(_ :) 的方法聲明。

**

@IBAction func addNewItem(_ sender: UIButton) {

**

@IBAction func addNewItem(_ sender: UIBarButtonItem) {

  

...

}

現在打開

Main.storyboard

然後打開對象庫。 将 導覽列按鈕項 拖動到

Items View Controller

導航欄的右側。 選擇此 導覽列按鈕項 并打開其屬性檢查器。 将

System Item

更改為

Add

(圖14.14)。

圖14.14系統欄按鈕項

第十四章——UINavigationController

右鍵從此 導覽列按鈕項 拖動到

Items View Controller

并選擇

addNewItem

:(圖14.15)。

圖14.15連接配接 addNewItem:action

第十四章——UINavigationController

建構并運作應用程式。 點選

+

按鈕,一個新行将出現在表格中。 現在我們來更換

Edit

按鈕。 視圖控制器包含的導覽列按鈕項會自動切換其編輯模式。 沒有辦法通過

Interface Builder

通路它,是以您需要以程式設計方式添加該按鈕項。

ItemsViewController.swift

中,重寫 init(coder :) 方法來設定左邊導覽列按鈕項。

required init?(coder aDecoder: NSCoder) {

  

super.init(coder: aDecoder)

  

navigationItem.leftBarButtonItem = editButtonItem

}

建構并運作應用程式,添加一些 item,然後點選

Edit

按鈕。 UITableView 進入編輯模式!

editButtonItem

屬性建立了一個标題為

Edit

的 UIBarButtonItem。 更好的是,這個按鈕帶有一個目标動作對:在點選時調用它的 UIViewController 上的setEditing(_:animated :)方法。

打開

Main.storyboard

。 現在,

Homepwner

具有全功能的導航欄,您可以擺脫标題視圖和相關代碼。 在表視圖上選擇标題視圖,然後按

Delete

此外,UINavigationController 将處理更新表視圖的插值。 在

ItemsViewController.swift

中,修改為以下。

override func viewDidLoad() {

  

super.viewDidLoad()

  

// Get the height of the status bar

  

let statusBarHeight = UIApplication.shared.statusBarFrame.height

  

let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)

  

tableView.contentInset = insets

  

tableView.scrollIndicatorInsets = insets

  

tableView.rowHeight = UITableViewAutomaticDimension

  

tableView.estimatedRowHeight = 65

}

最後,删除 toggleEditingMode(_ :) 方法。

@IBAction func toggleEditingMode(_ sender: UIButton) {

  

// If you are currently in editing mode...

  

if isEditing {

    

// Change text of button to inform user of state

    

sender.setTitle("Edit", for: .normal)

    

// Turn off editing mode

    

setEditing(false, animated: true)

  

} else {

    

// Change text of button to inform user of state

    

sender.setTitle("Done", for: .normal)

    

// Enter editing mode

    

setEditing(true, animated: true)

  

}

}

建立并再次運作。 舊的

Edit

Add

按鈕已經消失,留下了一個好看的 UINavigationBar(圖14.16)。

圖14.16 帶導航欄的Homepwner

第十四章——UINavigationController

青銅挑戰:顯示數字鍵盤

顯示 Item 的

valueInDollars

的 UITextField 的鍵盤是一個QWERTY鍵盤。 如果它是一個數字鍵盤會更好。 将 UITextField 的

Keyboard Type

更改為

Number Pad

。 (提示:您可以使用屬性檢查器在 storyboard 檔案中執行此操作。)

白銀挑戰:自定義 UITextField

建立 UITextField 的子類,并覆寫 getsFirstResponder() 和 resignFirstResponder() 方法(繼承自 UIResponder),以使其邊框樣式在成為第一個響應者時更改。 您可以使用 UITextField 的 borderStyle 屬性來完成此操作。 在 DetailViewController 中使用您自定義的文本字段。

黃金挑戰:推動更多視圖控制器

目前,Item 的執行個體不能更改其

dateCreated

屬性。 更改 Item ,使他們可以,然後在 DetailViewController 中的

dateLabel

下面添加一個帶有 “Change Date” 标題的按鈕。當點選此按鈕時,将另一個視圖控制器執行個體 push 到導航堆棧。 此視圖控制器應包含一個要修改的所選 Item 的

dateCreated

屬性的 UIDatePicker 執行個體。