UIWindow
是Cocoa框架的重要組件之一丙笋,所有的UIView
都要通過(guò)UIWindow
來(lái)進(jìn)行展現(xiàn)捐凭,沒(méi)有UIWindow
就沒(méi)有我們的界面淑廊。關(guān)于UIWindow
的介紹和與其他組件如UIViewController
UIView
之間的關(guān)系,有很多文章已經(jīng)說(shuō)得很清楚了旧烧,相信作為一個(gè)有經(jīng)驗(yàn)的iOS開(kāi)發(fā)者都應(yīng)該了解的比較清楚影钉。如果有一些不清楚的地方,這里給一個(gè)傳送門:UIWindow簡(jiǎn)單介紹掘剪。
很多時(shí)候我們的App只有一個(gè)用來(lái)展示我們界面的UIWindow
平委,而且這個(gè)UIWindow
通常是在你創(chuàng)建工程的時(shí)候自動(dòng)生成的,這就讓我們就算了解UIWindow
的基本原理夺谁,也會(huì)比較少的接觸到UIWindow
的實(shí)際使用廉赔。當(dāng)App復(fù)雜度逐漸提高的時(shí)候,一些特定的場(chǎng)景使用UIWindow
會(huì)得到很好的解決予权。所以昂勉,這篇文章就介紹UIWindow
的使用場(chǎng)景和方法,還包括一些實(shí)踐中潛在的一些坑扫腺。Let's Go!
一村象、使用場(chǎng)景
-
可能在任何界面彈出的視圖
這種場(chǎng)景是UIWindow
最主要的使用場(chǎng)景之一笆环,也是Cocoa本身對(duì)UIWindow
的使用場(chǎng)景之一,比如Alert提醒框厚者、自定義鍵盤躁劣、Loading框等。UIKit中的UIAlertView
和彈出鍵盤都是新建了一個(gè)UIWindow
库菲,這一點(diǎn)可以在Debug模式下設(shè)置斷點(diǎn)账忘,再點(diǎn)擊Debug View Hierarchey查看UIView的層次結(jié)構(gòu),清晰的看到當(dāng)前應(yīng)用有幾個(gè)UIWindow
圖中的
UITextEffectsWindow
指的就是鍵盤的window
熙宇。接下來(lái)就簡(jiǎn)單說(shuō)明一下如何使用UIWindow鳖擒,一般我們不會(huì)去直接繼承
UIWindow
,因?yàn)槲覀儾恍枰淖兺卣顾挠猛径莾H僅使用它烫止。通常把新建的window
作為viewController
的強(qiáng)持有屬性,代碼如下
class ModalViewController: UIViewController {
var newWindow:UIWindow?
var prevWindow:UIWindow?
func show(){//顯示界面
if newWindow == nil {
//暫存原來(lái)的keyWindow
self.prevWindow = UIApplication.sharedApplication().keyWindow
//新建UIWIndow
let uiwindow = UIWindow(frame: UIScreen.mainScreen().bounds)
uiwindow.rootViewController = self
uiwindow.makeKeyAndVisible()
self.newWindow = uiwindow
}
}
func dismiss(){//退出界面
self.prevWindow?.makeKeyAndVisible()
self.newWindow?.rootViewController = nil
self.newWindow = nil
}
}
上述代碼把原來(lái)的keyWindow
暫存,把我們新建的window設(shè)置成keyWindow
察署,在退出界面時(shí)恢復(fù)原來(lái)的keyWindow
,銷毀viewController
和newWindow
惊奇。由于keyWindow的官方定義是
The key window is the one that is designated to receive keyboard and other non-touch related events. Only one window at a time may be the key window.
keyWindow是指定的用來(lái)接收鍵盤以及非觸摸類的消息,而且程序中每一個(gè)時(shí)刻只能有一個(gè)window是keyWindow
如果你希望展示的viewController只需要接受觸摸事件播赁,不需要接受彈出鍵盤等非觸摸事件颂郎,你完全可以不把newWindow
設(shè)置成keyWIndow
,簡(jiǎn)單調(diào)用window.hidden = false
就可以把界面顯示出來(lái)容为,如下所示
class ModalViewController: UIViewController {
var newWindow:UIWindow!
func show(){//顯示界面
if newWindow == nil {
//新建UIWIndow
let uiwindow = UIWindow(frame: UIScreen.mainScreen().bounds)
uiwindow.rootViewController = self
uiwindow.hidden = false
uiwindow.backgroundColor = UIColor.clearColor()
self.newWindow = uiwindow
}
}
func dismiss(){//退出界面
self.newWindow.rootViewController = nil
self.newWindow = nil
}
}
-
獨(dú)立的祖秒、跨界面使用的服務(wù)
這類使用場(chǎng)景也比較常見(jiàn),比如錄音舟奠、仿Assistive Touch竭缝、懸浮窗等。以錄音為例沼瘫,錄音進(jìn)入后臺(tái)后抬纸,可能會(huì)在statusBar
上顯示一個(gè)紅色的正在錄音的提示框,這種效果用UIWindow來(lái)
實(shí)現(xiàn)就非常自然耿戚,因?yàn)樗粫?huì)影響到其他界面湿故,始終在自己的newWindow
里做相應(yīng)的操作,不用關(guān)心主窗口的頁(yè)面切換膜蛔。實(shí)現(xiàn)方式其實(shí)和第一種使用場(chǎng)景類似坛猪,也是強(qiáng)持有newWindow
的實(shí)例,不過(guò)有一個(gè)需要注意的地方:
- 設(shè)置
window.frame
為比UIScreen.mainScreen().bounds
小的值皂股,比如錄音的提示框墅茉,可能很自然會(huì)設(shè)置成statusBar.frame
,但這可能會(huì)影響UIWindow
的旋轉(zhuǎn)呜呐。為了避免這個(gè)問(wèn)題就斤,我們一般來(lái)說(shuō)都會(huì)把window.frame
設(shè)置成UIScreen.mainScreen().bounds
。
但是這樣帶來(lái)一個(gè)問(wèn)題就是我們的newWindow
把下層window
的觸摸事件都屏蔽了蘑辑。這一點(diǎn)在自定義AlertView
時(shí)可能是我們想要的結(jié)果洋机,畢竟我們不想在alert
彈框時(shí)用戶還能做其他的操作,但是在錄音的時(shí)候洋魂,我們希望用戶還能做其他的操作绷旗,這個(gè)時(shí)候正確的做法就是繼承UIWindow
,重載hitTest
方法副砍。
class ThroughView:UIView {
// 在你的xib/storyboard/代碼里衔肢,設(shè)置需要穿透的View為ThroughView
}
class ProgressWindow:UIWindow {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
//如果hitView是需要事件穿透的ThoughView則返回nil
if let _ = hitView as? ThroughView {
return nil
} else {
return hitView
}
return hitView
}
}
-
復(fù)雜的頁(yè)面切換中作為遮罩層防止閃屏。
這類用法不需要我們創(chuàng)建新的UIWindow址晕,而是當(dāng)我們遇到復(fù)雜的頁(yè)面切換時(shí)膀懈,調(diào)用當(dāng)前視圖的截屏方法snapshotViewAfterScreenUpdates
獲取屏幕截圖snapView
,再調(diào)用window.addSubview
方法添加snapView
谨垃,達(dá)到覆蓋下層的頁(yè)面切換效果启搂,防止閃屏硼控,頁(yè)面切換結(jié)束后removeFromSuperview
。這種方法在下面介紹的UIWindow切換rootViewController導(dǎo)致無(wú)法釋放中有使用到胳赌,具體的使用可以往后繼續(xù)看牢撼。
snapView = self.view.snapshotViewAfterScreenUpdates(false)
snapView.frame = self.view.frame
self.window?.addSubview(snapView)
二、潛在的坑
雖然UIWindow
非常適用于上述提到的幾種場(chǎng)景疑苫,但總體來(lái)說(shuō)熏版,我們使用到常規(guī)組件的頻率還是要比UIWindow
高出不少,這也就意味著當(dāng)你在使用UIWindow
過(guò)程中遇到了坑時(shí)捍掺,可能比較難找到相應(yīng)的解決辦法撼短。這里我也記錄一下我在使用UIWindow
的過(guò)程中踩的一些坑,都是屬于比較隱蔽而且資料較少的挺勿,希望能幫助到大家曲横。
-
UIWindow旋轉(zhuǎn)問(wèn)題
一般來(lái)說(shuō),我們創(chuàng)建一個(gè)UIWindow的時(shí)候都會(huì)把它的bounds
設(shè)置成主屏幕大小UIScreen.mainScreen().bounds
但我們知道不瓶,在iOS7上禾嫉,UIScreen.mainScreen().bounds
不會(huì)隨著設(shè)備旋轉(zhuǎn)方向而改變,iOS8以上則會(huì)隨設(shè)備旋轉(zhuǎn)方向改變蚊丐,即橫屏和豎屏狀態(tài)下熙参,寬和高會(huì)互換。所以麦备,一般為了兼容iOS7獲取主屏幕的bounds
的正確大小孽椰,我們會(huì)給UIScreen加一個(gè)extension/category
extension UIScreen {
var compatibleBounds:CGRect {//iOS7 mainScreen bounds 不隨設(shè)備旋轉(zhuǎn)
var rect = self.bounds
if NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 {
let orientation = UIApplication.sharedApplication().statusBarOrientation
if orientation.isLandscape{
rect.size.width = self.bounds.height
rect.size.height = self.bounds.width
}
}
return rect
}
}
所以在這里,你可能以為泥兰,如果應(yīng)用要兼容iOS7弄屡,那么應(yīng)該把window.bounds
設(shè)置為UIScreen.mainScreen().compatibleBounds
。但其實(shí)這樣做是錯(cuò)誤的鞋诗,正確的做法是恰恰是最原始的做法,即設(shè)置成UIScreen.mainScreen().bounds
迈嘹。
我沒(méi)有找到合理的解釋削彬,但是我覺(jué)得可能的原因是,旋轉(zhuǎn)事件的派發(fā)流程是:UIApplication
-> UIWindow
-> UIViewController
秀仲,UIWindow
能夠自己處理自己的旋轉(zhuǎn)問(wèn)題融痛,所以不需要我們?cè)僮鲱~外的操作。
-
UIWindow切換rootViewController導(dǎo)致無(wú)法釋放
當(dāng)我們需要從任何界面跳轉(zhuǎn)到一個(gè)viewController時(shí)神僵,并且釋放原來(lái)的viewController雁刷,如果使用通常的presentViewController,原來(lái)的viewController并沒(méi)有釋放保礼。這時(shí)候可以通過(guò)簡(jiǎn)單的改變window.rootViewController達(dá)到這個(gè)效果沛励。
let newVC= SomeViewController()
if let window = UIApplication.sharedApplication().keyWindow {
var oldVC = window.rootViewController
window.rootViewController = vc
oldVC = nil
}
那么责语,此時(shí)oldVC有沒(méi)有被釋放呢?
- 正常的情況下目派,oldVC如果是一個(gè)簡(jiǎn)單的viewController坤候,oldVC當(dāng)然是被釋放了,因?yàn)橐呀?jīng)沒(méi)有引用指向oldVC企蹭。
- 如果oldVC是
UINavigationController
或者UITabbarController
之內(nèi)的容器類白筹,oldVC和上述一樣,也會(huì)被釋放谅摄。但是這時(shí)候如果oldVC已經(jīng)push進(jìn)了幾個(gè)viewController徒河,這些viewController會(huì)被釋放么?答案是肯定的送漠。因?yàn)楫?dāng)viewController被容器類管理時(shí)顽照,只有容器類會(huì)持有viewController的引用,當(dāng)容器類被銷毀時(shí)失去引用也會(huì)被釋放了螺男。 - 當(dāng)oldVC調(diào)用了presentViewController模態(tài)彈出了viewController的時(shí)候棒厘,oldVC在iOS7上會(huì)被釋放,但是在iOS8以上的設(shè)備并沒(méi)有被釋放下隧。這個(gè)是我實(shí)踐得出的結(jié)果奢人,我覺(jué)得可能的原因是由于每個(gè)viewController都有這兩個(gè)只讀屬性presentedViewController和presentingViewController,代表彈出它和它彈出的viewController淆院,可能是這兩個(gè)屬性導(dǎo)致了循環(huán)引用何乎,所以得不到釋放。所以土辩,當(dāng)我們要切換一個(gè)已經(jīng)present了的控制器支救,我們需要把該控制器逐層dismissViewController(因?yàn)閜resent之后還可以繼續(xù)present),直到dismiss到根視圖拷淘。為了達(dá)到這個(gè)效果各墨,我寫了一個(gè)簡(jiǎn)單的extension。
extension UIWindow {
var safeRootViewController:UIViewController? {
get {
return self.rootViewController
}
set {
if let prevRootVC = self.rootViewController {
// Get TopMost VC
var topMostVC:UIViewController! = prevRootVC
while(topMostVC.presentedViewController != nil) {
topMostVC = topMostVC.presentedViewController
}
var window:UIWindow?
// 增加snapView防止閃屏
var snapView:UIView?
if topMostVC != prevRootVC {
snapView = topMostVC.view.snapshotViewAfterScreenUpdates(false)
snapView?.frame = UIScreen.mainScreen().compatibleBounds
self.addSubview(snapView!)
}
func dismissToRootVC(topMostVC:UIViewController,complete:() -> Void) {
// 獲取present topMostVC的VC
if let presentingVC = topMostVC.presentingViewController {
topMostVC.dismissViewControllerAnimated(false) {
dismissToRootVC(presentingVC,complete: complete)
}
} else {// 說(shuō)明topMostVC沒(méi)有被present启涯,已經(jīng)dismiss到最底層了
complete()
}
}
dismissToRootVC(topMostVC) {
self.rootViewController = newValue
// 延遲執(zhí)行贬堵,等待UI更新界面
self.performSelector(#selector(UIWindow.delay(_:)), withObject: snapView, afterDelay: 0)
}
} else {
self.rootViewController = newValue
}
}
}
func delay(snapView:AnyObject?) {
(snapView as? UIView)?.removeFromSuperview()
}
}
增加snapView的目的是防止閃屏,因?yàn)槲覀円谥饘觗ismiss到根視圖后结洼,再切換viewController黎做,這就會(huì)造成頂層視圖到根視圖之間的視圖一閃而過(guò)。具體的實(shí)現(xiàn)代碼注釋部分已經(jīng)說(shuō)明的比較詳細(xì)了松忍,就不再贅述蒸殿。
-
UIWindow不顯示View
這個(gè)問(wèn)題嚴(yán)格上說(shuō)不是UIWindow本身的問(wèn)題,而是出現(xiàn)在使用轉(zhuǎn)場(chǎng)動(dòng)畫UIViewControllerContextTransitioning
時(shí),在動(dòng)畫結(jié)束之后宏所,待顯示的viewController.view
沒(méi)有被自動(dòng)添加到UIWindow
上酥艳,導(dǎo)致顯示為黑屏或空白,這是Cocoa本身的bug楣铁。解決的方法就是在轉(zhuǎn)場(chǎng)動(dòng)畫結(jié)束之后手動(dòng)把view
添加到keyWindow
上玖雁。
transitionContext.completeTransition(true)
UIApplication.sharedApplication().keyWindow!.addSubview(toViewController.view)
對(duì)應(yīng)stack overflow上的這個(gè)問(wèn)題:“From View Controller” disappears using UIViewControllerContextTransitioning
三、小結(jié)
回顧一下盖腕,寫這篇文章的一個(gè)原因就是我發(fā)現(xiàn)介紹UIWindow
的實(shí)際使用的文章非常少赫冬,所以在此總結(jié)一下常見(jiàn)的使用場(chǎng)景和方法,還有就是使用UIWindow遇到的坑溃列。這篇文章的內(nèi)容大部分是我在實(shí)踐中結(jié)合UIWindow
的原理總結(jié)出的一些經(jīng)驗(yàn)劲厌,自己留一篇記錄加深印象,同時(shí)也希望對(duì)大家有所幫助听隐!