close

大家好,今天要分享的是史丹佛大學課程,第七堂課的內容。

相對於前面相對簡單的語法語介面操作示範,這一堂課開始深入大型 App 的架構,利用多層次的 MVCs 來實現比較複雜的 App 。

這堂課的內容主要是在講解 Split View Controllers 、 Navigation Controllers 、 Tab Bar Controllers Segues 、 Segues 和 Popovers 。

下面附上第七堂課程在 Youtube 的連結,大家也可以自行在 iTune U 上搜尋觀看。



一個 MVC 只能架構出一個最簡易的功能的 APP ,如果想要擁有一個功能更強大的 App ,勢必要結合多個 MVC 才能辦到。

要如何將多個 MVC 組合在一起呢?

iOS 提供了幾個有用的 Controller 來幫助我們完成這項工作,最基本的如下面三個。

1. UITabBarController 

2. UISplitViewController 

3. UINavigationController 


首先談談 UITabBarController ,他可以讓我們將多個 MVC 用一個 Tab Bar 統整在一個畫面裡,藉由選擇 Tab Bar 裡的項目來切換至對應的 MVC 。

而每一個 MVC 彼此之間最好是獨立不相互依賴的,雖然編程上不會出現錯誤訊息通知,但這不是一個好的習慣,會導致之後出問題的時候相當難以處理。

所以結論是,當我們有幾個在組織結構上屬於同層級的 MVC 要展示的時候,我們就使用這個控制器來實現它。

除了下圖的範例之外,我這邊在另外舉個例子幫助大家理解。

假如我想要設計一個娛樂控制器,我會有電影、音樂、遊戲和書籍幾個大類別,這四個類別並沒有層級上下高低的分別,是完全平行的四個分類,這時候我們就可以用 UITabBarController 來統整。


接著談談 UISplitViewController ,它只包含兩個 MVC ,一個是 Master ,一個是 Detail 。

如下面的計算機為例,左邊的數字控制界面是 Master ,右邊的圖形顯示界面是 Detail 。

這麼區分所代表的意思是,作為 Master 的數字控制界面發出訊息控制右邊作為 Detail 的圖形顯示介面畫出圖形,是一個從屬的關係。

利用這個工具,我們可以將畫面分割為左右兩部分,這樣一來就可以在動作的同時,即時得到反應結果。

不過這個功能只能在螢幕尺寸夠大的設備上才能夠使用,例如 iPAD 和 iPhone 6 plus 才有足夠的畫面支援這樣的顯示模式。


最後要談的是 UINavigationController  ,它是一個單一的 MVC 結構,不同的是它的 View 是由其他的 MVC 所組成,並且可以同時包含複數的 MVC 。

但它的做法並不是將這些 MVC 同時放置在一個畫面裡,而是將它們作堆疊處理,需要用到的時候再移到最上層,也就是當下的顯示畫面上。

以下面的 Setting 介面來說明, NavigationController 只決定了最上面的 Title ,下面的那些選項介面由目前使用的 MVC 所決定。

但 NavigationController 能夠幫助我們將每一個選項, General 、 Privacy 、 iCloud 、 Maps ...等等連結到其他的 MVC 。

也就是說,當我們點擊 General 的時候,NavigationController 就會將 General 的 MVC 切換到 View 的最上層做顯示。


講到這裡,我們又不得不提一個重要的東西, Segues

Segues 是一個將一個 ViewController 切換到另一個 ViewController 的方式。

iOS 定義了四種基本的 Segues ,分列如下:

1. Show Segue (will push in a Navigation Controller, else Modal)

展示我們當下正在溝通使用的 MVC 。假設我們是一個 NavigationController ,那個這個 MVC 就會被擺置到最上層的顯示畫面上。如果不是則使用 Modal 將這個 MVC 佔滿整個螢幕地顯示。

2. Show Detail Segue (will show in Detail of a Split View or will push in a Navigation Controller) 

功能與 Show Segues 差不多,差別在於它是將 MVC 展示在 SplitViewController 的 Detail 視窗中。

3. Modal Segue (take over the entire screen while the MVC is up)

將我們想要展示的 MVC 完全佈滿整個螢幕空間。

4. Popover Segue (make the MVC appear in a little popover window) 

將彈出一個顯示目標 MVC 的視窗,這個視窗只會在視覺上佔據螢幕的一部份,但實際上它是完全佔滿整個螢幕的。當它顯示的時候,我們無法操作不屬於這個 MVC 的畫面,當我試圖點擊的時候,這個 MVC 就會結束。


再來我們順便提一下 Identifier 這個東西。

StoryBoard 的所有東西都是依靠名字與我們的編碼聯繫在一起的,當然 Segues 也不能免除。

在 Segues 中最常會使用到 Identifier 的理由就是為了準備一個 Segues 。

當我們利用 Segues 轉換到一個新的 MVC 時,這個 MVC 是新創造出來的,我們必須先準備一些敘述告訴這個 MVC 它的任務是什麼,於是這個 MVC 才能進行它的動作。

怎麼進行這個準備動作呢?

MVC 裡面有一個觸發 Segues 的物件,例如 Button ,我們可以使用一個方法 prepareForSegue ,裡面存儲著剛創建出來的 MVC ,裡面的程式碼如下所示。

func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

      if let identifier = segue.identifier {

          switch identifier {

              case “Show Graph”:

                  if let vc = segue.destinationViewController as? MyController {

                      vc.property1 = ...

                      vc.callMethodToSetItUp(...)

                  } 

              default: break } } }

這裡有兩個東西相當重要,分別是 Identifier 和 destinationViewController 。

Identifier 是我所需要準備的 Segues 的名字, destinationViewController 是即將被創造的 MVC 。

另外還要注意到另一個東西, sender ,它是觸發 Segues 的物件。

當我們使用 prepareForSegue 時需要注意到一件事情,那就是此時這個 MVC 的 outlet 還沒被設定,如果依然執行的話,基本上程序會崩潰。

It is crucial to understand that this preparation is happening BEFORE outlets get set! 


下面開始進行實作,我們要將之前所寫的笑臉 APP 應用到多重 MVC 的範例之中。

目的是設計一個控制視窗,可以根據選項的不同來得到不同的開心程度的笑臉。

上面的程式碼中,我們重寫了一個 perpareForSegue ,因為我們要藉由觸發在 StoryBoard 上的 Button 來控制我們的笑臉改變它的開心程度 (happiness) 。

第一步宣告一個 hvc 常量,令其代表 segue 即將創造的 MVC ,並將這個 MVC 轉換為 HappinessViewController 類型,如果可以轉換在進行下面的動作。

第二步宣告一個 identifier 常量,並假設它等於 segue 對應的 identifier 時進行下面的動作。

第三步利用 switch 的功能,分別按照觸發 Button 的 identifier 設定幾種 case ,讓其執行對應的動作。

就是將 happinessViewController 內的 happiness 的值做其他宣告。

以上程式碼完成後就可以讓我們可以藉由點擊按鈕控制笑臉的變化。

但這裡還有個小缺陷會導致我們的程序崩潰,那就是 outlet 還沒有被設定的問題,於是我們需要在 HappinessViewController 做一些修改。

我們所做的修改其實只是在 faceView 後面加一個問號而已,將其變成可選的。

也就是說,如果因為 outlet 尚未被設置導致它在執行重新繪製的時候崩潰,那麼我們就設置為可以被忽略,之後等他即時更新時就有 outlet 被設置,然後就可以正常工作了。

粗略點的解釋大概就是這樣子。


另外我們還可以順便把笑臉的 View 設置一個 title 為 happiness 的值,但是僅僅只加上 "title = "\(happiness)" 這段敘述會導致程式不工作。

我們必須再增加一段程式碼。

我們先將 segue.destinationViewController 轉換為 UIViewController 並宣告為一個變量 destination ,以便之後的其他轉換方便。

接著我們再宣告一個常量 navCon 為 destination ,並將其轉換為 UINavigationController 類型 。假設轉換成立的話則執行下面動作。

讓 destination 等於 navCon.visibleViewController! 。

visibleViewController 的文檔內容如下。

Declaration : var visibleViewController: UIViewController? { get }

Description : The view controller associated with the currently visible view in the navigation interface. (read-only)

The currently visible view can belong either to the view controller at the top of the navigation stack or to a view controller that was presented modally on top of the navigation controller itself.

這樣做的意思是,當我們的 segue.destination 指向 UINavigationViewController 時,我們就將 destination 宣告為 NavigationViewController 當下最上層顯示的 ViewController ,這我們的例子中就是 HappinessViewController ,這樣就可以讓 HappinessViewController 在埋進 NavigationController 後依然正常工作了。


有時候可能會有某些原因導致我們想要用編碼的方式來設定 segue ,而不是藉由 StoryBoard 來拖曳連結。

那麼也可以,只需要另外做宣告就可以了。


我們還有一個 popover 還沒有講,下面就針對這個工具另外做介紹。

popover 雖然與其他三個一樣都是 segue 的一種,但是不同於其他三者的是它不屬於 UIViewController ,它屬於它自己,但它依然可以被 UIPresentationController  所控制。

它的程式碼如下所示。

func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 

     if let identifier = segue.identifier {

         switch identifier {

             case “Do Something in a Popover Segue”:

                 if let vc = segue.destinationViewController as? MyController {

                     if let ppc = vc.popoverPresentationController {

                         ppc.permittedArrowDirections = UIPopoverArrowDirection.Any

                         ppc.delegate = self  } 

// more preparation here 

             default: break } } } 


所以當我們要創造一個 popover 時,必須專門建立一個全新的 MVC 來作為它的功能程式。

於是我們創造一個新的 UIViewController 叫做 TextViewController 。

這是一個很簡單的程式碼,我們只想創造一個可以顯示診斷歷史訊息的 popover ,所以我們的 View 只需要一個文檔 textView。

宣告一個變量 text 並令其為字串類型,接著利用屬性監測器讓它回傳更新後的資料到 textView 的顯示畫面上。

同樣在 textView 的 outlet 也做同樣的設置,不過這邊我實驗發現,屬性監測器只需要在 outlet 的地方設置就可以, text 變量不需要使用也不會造成程式出問題。


接著我們要導入笑臉的 happiness 數值來作為 text 的值,但我不能直接從 HappinessViewController 來導入,因為從設計者的角度來看,HappinessViewController 只是用來顯示不同快樂程度的笑臉的 APP 。雖然執行上不會出問題,但是避免各個部分的功能混亂是編程基本概念。

所以一個變通的方法就是創造一個 HappinessViewController 的子類 DiagnosedHappinessViewController ,這個子類會完美繼承 HappinessViewController 的所有屬性,包括其與 StoryBoard 的各個連接關係,且不會影響父類的正常工作。

於是我們在另外創造一個 Swift File 來建立一個新的 Class ,並讓他繼承 HappinessViewController ,然後再將 happiness 修改後給 popover 使用。

如上面所寫的,我們先創造一個名為 diagnosticHistory 的 Array 作為 popover 所要展示的歷史診斷訊息的內容。

接著導入 segue 的程式碼,設定當常量 identifier 等同於 popover 所設定的 identifier 時,執行下面動作。

若是 identifier 為 Show Diagnostic History ,則將 tvc (代表 TextViewController 的新常量) 的屬性 text 宣告為前面所宣告的變量 diagnosticHistory 。

並改寫父類的 happiness 屬性的屬性觀測器,當發現 happiness 的數值改變時,將新的值寫入 diagnosticHistory 的陣列裡面。

於是 popover 就可以得到歷史診斷結果。


但這裡依然有個問題尚未解決,那就是目前 popover 內的歷史診斷結果只能顯示最新一次的數據。

這是因為 popover 每次被觸發時,都會建立一個全新的 MVC ,上一個 MVC 包含它所儲存的資料都會被清空。

所以這時候我們就要利用到之前學的 NSUserDefault 。

先建立一個用來儲存歷史診斷訊息的區塊 defaults ,並定義它為 NSUserDefaults.standardUserDefaults() 。

standardUserDefaults() 的文檔內容如下。

Declaration : class func standardUserDefaults() -> NSUserDefaults

Description : Returns the shared defaults object.

The shared defaults object.

Returns : The shared defaults object.


然後用屬性觀測器重新定義 diagnosticHistory 。

這邊會用到兩個屬性方法, setObjectobjectForKey

setObject 文檔內容如下。

Declaration : func setObject(value: AnyObject?, forKey defaultName: String)

Description : Sets the value of the specified default key in the standard application domain.

The object to store in the defaults database.

Parameters :

value : The object to store in the defaults database.

defaultName : The key with which to associate with the value.


objectForKey 文檔內容如下。

Declaration : func objectForKey(defaultName: String) -> AnyObject?

Description : Returns the object associated with the first occurrence of the specified default.

A key in the current user's defaults database.

The returned object is immutable, even if the value you originally set was mutable.

Parameters :

defaultName : A key in the current user's defaults database.

Returns : The object associated with the specified key, or nil if the key was not found.


利用屬性觀測器的 setter 將新得到的 diagnosticHistory 值存入 defaults 裡,並設置對應的 Key 為 History.defaultskey

再利用 getter 將 defaults 的值用前面設置的 Key 取出來給  diagnosticHistory

由於 defaults 是 NSUserDefaults 類型的資料,它不會因為 App 的關閉等變化導致資料消失,所以當 popover 重新創造一個 MVC 時,它的資料會依然存在,與新的歷史訊息累放在一起。


到這裡,我們已經完成了一個具有歷史診斷紀錄的 popover 功能與能夠利用 segue 觸發笑臉的開心程度的複合式 MVC 了。

但還有最後一個問題需要我們的改良,那就是 popover 在螢幕尺寸比較小的 iPhone 上的顯示問題。

通常 popover 只會佔用螢幕的部分空間,但因為 iphone 5 之前的型號尺寸都過小,導致 popover 展開之後會佔據整個螢幕畫面,連取消都做不到,所以我們要修正這個問題。

我們新增了一個條件,如果我們進入 popover ,則執行下面的動作。

執行自己的代理協議。

 if let ppc = tvc.popoverPresentationController { ppc.delegate = self  } 

這裡的協議我們要在最上面的類型宣告那邊新增定義,UIPopoverPresentationControllerDelegate

這個協議可以讓我們改變 popover 的顯示型態,將預設 modal 改為 none 。

 func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle { return UIModalPresentationStyle.None }

這樣一來,在小尺寸的螢幕顯示時也不會佔滿整個畫面了。

但我們可以再做得更好,藉由在 TextViewController 增加一段敘述,可以讓 popover 的視窗大小符合敘述的內容所需尺寸。

下面先針對幾個新的東西提供文檔敘述。

preferredContentSize 的文檔內容如下。

Declaration : var preferredContentSize: CGSize { get set }

Description : The preferred size for the view controller’s view.

The value in this property is used primarily when displaying the view controller’s content in a popover but may also be used in other situations. Changing the value of this property while the view controller is being displayed in a popover animates the size change; however, the change is not animated if you specify a width or height of 0.0.


presentingViewController 的文檔內容如下。

Declaration : var presentingViewController: UIViewController? { get }

Description : The view controller that presented this view controller. (read-only)

When you present a view controller modally (either explicitly or implicitly) using the presentViewController:animated:completion: method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.


sizeThatFits 的文檔內容如下。

Declaration : func sizeThatFits(size: CGSize) -> CGSize

Description : Asks the view to calculate and return the size that best fits the specified size.

The size for which the view should calculate its best-fitting size.

Parameters: 

size : The size for which the view should calculate its best-fitting size.

Returns : A new size that fits the receiver’s subviews.


這一段敘述的內容將 preferredContentSize 這個父類屬性改寫,它代表的是預設的 popover 螢幕尺寸大小。

利用屬性監測器,先設定每得到一個 newValue 都宣告為 父類的 preferredContentSize

再來設置一個條件,當文檔內有內容且當下正在開啟 popover 的畫面時,將返回最適合當下內容尺寸的畫面大小給 textView 來顯示,否則回傳 父類的預設值。

這樣一來, popover 跳出來的畫面就會完全符合我們的內容多寡了。



今天的內容就到此告一段落,我們下堂課見。

By 宅宅阿軒

arrow
arrow
    文章標籤
    iOS8 Swift
    全站熱搜

    Cloud 發表在 痞客邦 留言(0) 人氣()