UIWindow實(shí)踐

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

Debug View Hierarchy.png

Debug Session.png

圖中的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,銷毀viewControllernewWindow惊奇。由于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ì)大家有所幫助听隐!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末补鼻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雅任,更是在濱河造成了極大的恐慌风范,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪么,死亡現(xiàn)場(chǎng)離奇詭異硼婿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)禽车,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門寇漫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人殉摔,你說(shuō)我怎么就攤上這事州胳。” “怎么了逸月?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵栓撞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我碗硬,道長(zhǎng)腐缤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任肛响,我火速辦了婚禮,結(jié)果婚禮上惜索,老公的妹妹穿的比我還像新娘特笋。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布猎物。 她就那樣靜靜地躺著虎囚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蔫磨。 梳的紋絲不亂的頭發(fā)上淘讥,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音堤如,去河邊找鬼蒲列。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搀罢,可吹牛的內(nèi)容都是我干的蝗岖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼榔至,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抵赢!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起唧取,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铅鲤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后枫弟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體邢享,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年媒区,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驼仪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袜漩,死狀恐怖绪爸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宙攻,我是刑警寧澤奠货,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站座掘,受9級(jí)特大地震影響递惋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜溢陪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一萍虽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧形真,春花似錦杉编、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘶朱。三九已至,卻和暖如春光酣,著一層夾襖步出監(jiān)牢的瞬間疏遏,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工救军, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留财异,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓缤言,卻偏偏與公主長(zhǎng)得像宝当,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胆萧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 重點(diǎn)參考鏈接: View Programming Guide for iOS https://developer....
    Kevin_Junbaozi閱讀 4,440評(píng)論 0 15
  • 7庆揩、不使用IB是,下面這樣做有什么問(wèn)題跌穗? 6订晌、請(qǐng)說(shuō)說(shuō)Layer和View的關(guān)系,以及你是如何使用它們的蚌吸。 1.首先...
    AlanGe閱讀 674評(píng)論 0 1
  • UIView的功能 負(fù)責(zé)渲染區(qū)域的內(nèi)容锈拨,并且響應(yīng)該區(qū)域內(nèi)發(fā)生的觸摸事件 UIWindow 在iOS App中,UI...
    小蘑菇2閱讀 777評(píng)論 4 5
  • 一羹唠、簡(jiǎn)介 <<UIWindow類定義奕枢,管理和協(xié)調(diào)的Windows應(yīng)用程序顯示在屏幕上的對(duì)象(如Windows)。一...
    無(wú)邪8閱讀 1,361評(píng)論 2 3
  • 四月九號(hào)佩微,《歡樂(lè)喜劇人》第三季收官缝彬,遼寧民間藝術(shù)團(tuán)的文松團(tuán)隊(duì)獲得了第三季冠軍。 其實(shí)這樣的節(jié)目誰(shuí)拿冠軍都無(wú)所謂哺眯,重...
    酒言醉語(yǔ)閱讀 1,007評(píng)論 14 8