The Window
作者:PMST
文章:Views - Window
系列:The Swift Beginner
寫於:2015.04.25
該系列更多文章詳見個站Colourful Code
正文
前言講到App由多個 Views 構建顯示而成,稱之爲視圖層級(view hierarchy),那麼不禁要問,它的最頂層是什麼?答案是: Window。 一個通過 UIWindow類
或者繼承 UIWindow的子類
的實例
,特別要提出的是 UIWindow 是繼承自 UIView 。Ps:你可以找個項目打開AppDelegate.swift
文件,其中有個var window:UIWindow?
變量聲明,option+鼠標左擊
UIWindow,彈出關於它的信息,其中 “Declaration class UIWindow : UIView” 證明了前面所說。
Ok,關於App,確切地說只有一個主窗口(main window),在launch的時候創建,此後你無法對它銷燬或者替換。主窗口會佔據整個窗口,將自身作爲整個背景(background),這個很好理解,因爲我們看的確實如此。相對於其他所有可見的視圖,main window 自然就是superView嘍,其他視圖作爲subView呈現自身內容,這點是需要記住的。
Window 必須要填充滿整個設備的屏幕,意味着它的尺寸大小和放置位置要和屏幕的尺寸和位置相統一。還記得前面提到創建的UIWindow實例嗎,通過設置它(實例,通常取名爲:window
)的frame屬性等於屏幕(Screen)的bounds即可,對於frame 和bounds 這兩個屬性,會在之後的一節進行解釋。關於設置問題我們從兩種情況出發,使用storyboard 和使用代碼。
使用 a main storyboard ,在app launch之前,它會非常貼心地爲你完成這些繁瑣的設置,比如尺寸、放置位置等等。
使用代碼的情況下, app 要求我們去手寫代碼設置 window 的 frame,就像這樣
let w = UIWindow(frame:UIScreen.mainScreen().bounds)
。假如你有編程基礎,解讀起來一定很容易;如果是初學者也沒有關係,仔細看這行代碼,和我前面所說是一樣的,構造一個frame和屏幕bounds一樣的實例窗口(window)。
值得一提的是,在你應用程序的生命週期裏(lifetime,這個概念比較重要),先前window實例將一直存在。爲了達到這個目的,應用的delegate類中有一個window屬性,採用強引用(strong retain)指向先前的window實例。具體是這麼實現的:當應用程序啓動時,UIApplication函數實例化一個代理(delegate)並且將生成結果保持住,對於app delegate來說,它永遠不會被釋放(released),在應用程序的生命週期裏將一直存在;接着,window實例會被signed
給delegate中的window屬性,採用強引用的方式,意味着在app的整個生命週期,只要delegate不消亡,那麼它之中的window屬性也不回消亡。
補充:第一點:window實例如何signed給delegate中的window,很簡單,就類似代碼中的賦值操作,有點像
delegate.window = w
,w是先前生成的窗口實例。第二點:對於強引用,也就是strong reference,首先對於類之間的賦值,不是簡單的值拷貝方式,而是採用引用方式,即都指向一個對象,其次是strong關係,必然是在兩個對象之間發生,只要強引用一方不解除關係,另外一方就要一直存在。
要呈現app內容,將所有視圖內容手工地、直接地放入主窗口(main window)?Say NO!
,請你最好馬上現在摒棄這種思想,相應地,獲得一個View Controller 即視圖控制器,然後分配給主窗口(main window實例)中的rootViewController屬性。不得不再次提及,假如你使用SB的話,這些設置都是在背後悄悄幫你完成的;而view controller則將成爲storyboard的初始控制器(initial view controller),有些細心的朋友已經在SB中發現有一個箭頭標誌,其實右側面板對應一個屬性設置check框,用來指派是否成爲初始控制器(Is Initial View Controller
)
當視圖控制器成爲了主窗口(main window實例)的rootViewController之後,該視圖控制器中的主視圖(main view)立即成爲主窗口的subview——當然也就是主窗口的root view。而其他在窗口中的視圖將會成爲該root view的 subview。貌似有點繞口,但是如果你多看幾遍,便會發現很好理解。有時候我們好奇在root view 下的 window “過得怎樣”,抱着該目的會設置main window的背景顏色(backgroundColor)。這裏有個狀況,我再測試時候兩個工程,一個設置了背景卻沒有效果,另一個卻是紅色的,我抽空會再確認下!
凡事都是需要一個過程,應用程序的視圖也不會立即呈現,直到將window包含進來,且設置成爲app的 key window。代碼設置即調用UIWindow的實例方法makeKeyAndVisible
,從方法名就能解讀其作用。
ok,是時候總結一下初始化時所要做的創建、配置以及如何將window顯示出來。下面從使用storyboard 和未使用storyboard兩種情況來講解:
使用a main storyboard的情況
打開項目,找到Info.plist文件中鍵值爲“Main storyboard file base name”,值爲”Main”,對應我們項目中的Main.storyboard。值得一提的是對於所有Xcode 6 app都是默認如此。接下來要說的是,UIApplicationMain(你可以在appDelegate.swift文件找到)中會實例化一個UIWindow(如果你沒有自定義window,內部自動構造一個實例),正確設置frame屬性,然後賦值給app delegate中的window變量屬性(也就是 lazy var window : UIWindow?
的window,引用關係),與此同時,它還會實例化storyboard中的view Controller(通過SB中Identity Inspector看到,class下看到是ViewController,這是默認的),然後分配給window實例的rootViewController屬性,這些都是後臺執行,你看不到的。The one more thing,所有這一切都發生在delegate 的 application:didFinishLaunchingWitchOptions:
調用之前!!!!最後,UIApplicationMain 調用makeKeyAndVisible
方法使得我們的window擺正位置,呈現應用界面。之後輪到root view controller去獲得main view(通常我們會從nib文件中加載得到),將主視圖作爲window的root view。這個過程發生在application:didFinishLaunchingWitchOptions:
調用之後!!
未使用a main storyboard的情況
這對於用慣了storyboard的朋友無疑是“新大陸”,創建和配置window方式也從可視化往純代碼發展。假如想要創建一個無storyboard的工程,很遺憾,在Xcode 6 app中是沒有的,都是默認創建一個main storyboard,可見蘋果公司是支持並鼓勵用SB結合代碼方式來完成項目。因此我們只能在Single View Application模板上進行修改達到我們所要的目的。你可能需要動手完成以下步驟。
- 打開項目,選中你的target(藍色工程文件.xcodeproj),在General面板找到
Main Interface
,你會看到默認是Main,刪除它,回車或tab鍵確認。 - 刪除main.storyboard文件和ViewController.swift文件。
- 刪除AppDelegate.swift中原先的代碼,全部刪掉都ok。
如此就得到是一個沒有storyboard和任何代碼的空項目。現在我們添加一些必要代碼使其變成一個能夠運行的最小應用。首先選中AppDelegate.swift文件,寫入必要代碼(請見下)來實現創建window以及顯示window。爲了讓證明我們的的確確創建了一個window,你可以使用設置其背景顏色爲紅色(UIColor.redColor()
)。由於沒有設置一個root view controller 因此我們的調試區域會給出警告“Application windows are expected to have a root view controller at the end of application launch”。別緊張,“It’s Ok!” 接下來的文章便會告訴你如何實現。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window!.backgroundColor = UIColor.redColor()
self.window!.makeKeyAndVisible()
return true
}
}
以上就是一個能運行的最小應用了,你可以嘗試在模擬器運行,會看到紅色的一個界面,它就是window。
一般情況下我們這麼實現就已經OK了,但是假如你玩性大發,較真地想要自定義一個window,通過繼承UIWindow創建其subclass,然後實例化一個自定義window作爲應用的主窗口。既然講了,那麼就講到底,這裏也分兩種情況。
自定義創建window下,使用a main storyboard的情況
首先應用程序啓動,在UIApplicationMain實例化了一個delegate之後,app會向delegate索要它當中的window屬性(前文已經詳細講過),假如值不存在(nil),那麼UIApplicationMain就會自動實例化一個,然後將生成實例分配給window屬性變量;如果值是存在的,那麼UIAplicationMain就隨他去嘍,就按照那個存在的window作爲應用的主窗口。所以,假如我們我們構造了一個UIWindow subclass的實例作爲應用的主窗口,那麼自然還缺少一步:將生成的實例分配給delegate當中的window屬性。代碼實現:
lazy var window:UIWindow = {
return MyWindow(frame:UIScreen.mainScreen().bounds)
}
沒了?……抱歉我想是的!至於其他的就交給後臺去做吧!至於MyWindow就需要你自己新建一個Cocoa class文件,命名MyWindow,強調它是繼承自UIWindow的!
自定義創建window下,不使用a main storyboard的情況
作爲代碼實現,想想最最前面我們創建的那個沒有使用SB的例子,做的很好!可惜使用的是UIWindow,現在我們想要使用MyWindow,又該如何做呢?首先實例化一個自定義MyWindow然後分配給delegate當中的self.window屬性。就像這樣:
self.window = MyWindow(frame:UIScreen.mainScreen().bounds)
獲取window的方式
一旦應用運行,這裏存在兩種方式去獲得window。
如果當前界面中顯示了某個UIView,那麼該view自動產生一個引用指向window,換句話說該view中存在一個window屬性!!調用方式自然就是
view.window
。不過這裏不得不遺憾地告訴你,如果當前的view沒有現在在window中,那麼它的window屬性是等於nil的!!聰明的朋友已經意識到了,那麼反過來說如果一個view的window屬性等於nil,這個視圖就不可能在窗口中呈現!前面總是說app delegate始終保持了一個對window的強引用。因此我們採用迂迴戰術,通過shared application‘s delegate屬性獲得delegate的引用,然後通過它簡介獲得window。代碼是這樣的:
let w = UIApplication.sharedApplication().delegate!.window
//假如你覺得這個隱式解包麻煩 可以那麼寫
let w = (UIApplication.sharedApplication().delegate as AppDelegate).window
- 還有一種獲得機制是因爲shared application 通過keyWindow對window進行了引用保持,至於原因前面已經說過,代碼是這樣實現的。
let w = UIApplication.sharedApplication().keyWindow
不過呢,第三種引用稍微有個問題,因爲系統有時候會暫時生成一個window,然後將其插入到應用key window。因此大家還是需要引起注意的!