版本
Xcode 10.2
iPhone 6s (iOS12.4)
( 本文示例所用測試版本如上, 一些方法結論可能不適用於較舊版本的iOS/Xcode, 如需使用應先測試驗證. )
目錄
繼承關係
UIWindow : UIView : UIResponder : NSObject
結構
新建一個”Single View App”模板App, 點擊Debug view hierarchy按鈕, 打開的層級關係圖如下:
簡介
The backdrop for your app’s user interface and the object that dispatches events to your views.
編者譯: UIWindow對象充當了App中UI界面的背景(容器/載體), 還有一個作用是分派事件給各種view.
根據Apple官方文檔的這段話, 可以延伸爲以下兩點:
- App中任何一個view, 只有添加到相應的window中, 才能顯示出來;
- 觸摸事件會被傳遞到觸摸區域內的最上層的window, 非觸摸事件會被傳遞到keyWindow (詳見後文), 並由window將事件分發給恰當的view.
一般來說, 一個App只有一個window. 當我們使用Single View App模板來創建App的時候, 系統會幫我們創建好一個Main.storyboard, 一個ViewController (關聯storyboard裏面的VC), 以及一個AppDelegate等. 在AppDelegate.h文件中, 會有一個window屬性, 這個屬性的rootViewController就是前面的ViewController, 而且把ViewController的View添加到了window中, 這才把View顯示出來, 如上圖所示. 只不過這一系列的操作都是隱藏的, 所以一般我們很少和window打交道.
一個App只能有一個window嗎? 答案是否定的. App可以有很多window, 要麼是我們自己創建的, 要麼是系統幫我們創建的, 下文列舉一二.
App中有哪些常見window
- 主window, 即AppDelegate.h裏面的window, 由系統創建或者我們自己創建, 用來呈現App內容;
- 當使用UIAlertView或者UIAlertViewController創建一個提示彈框的時候, 系統會創建一個window: UITextEffectsWindow
- 當彈出系統鍵盤的時候, 系統創建兩個window: UITextEffectsWindow 和 UIRemoteKeyboardWindow
- 手機狀態欄的UIStatusBarWindow, 屬於系統級別, 不被App所持有;
- 我們自己創建的一些window, 比如登錄界面, 加載界面, 自定義提示框, 自定義鍵盤, 懸浮球, 錄音狀態欄等等.
- 外接屏幕需要新建一個window來顯示, 如投影到電視設備等.
注: 關於第2點, 使用UIAlertView創建提示框, 除了新增UITextEffectsWindow, 其實還有_UIAlertControllerShimPresenterWindow, 這貨變成了keyWindow, 不過不在App的windows列表中, 後文另有介紹.
window的創建
前文提到, 使用系統模板創建App, 系統會自動創建window. 大概流程是:
- 程序入口main(),
- 調用UIApplicationMain方法創建UIApplication對象(默認爲AppDelegate),
- 根據Info.plist裏”Main storyboard file base name” (等同targets中的Main interface選項) 對應的名稱作爲main storyboard並加載之,
- 偷偷摸摸地實例化AppDelegate.h中的window,
- 將Main.storyboard裏的Initial View Controller (默認爲ViewController)設爲window的rootViewController, 此時相當於把ViewController的View添加到window中,
- 執行makeKeyAndVisible方法將window設爲keyWindow並使其可見(hidden=NO),
- 創建完成, 顯示.
本來想新建一個空的project來從頭演示window的創建, 但是新版Xcode默認不允許創建空的項目. 無奈只好先創建模板App, 然後刪除默認storyboard/AppDelegate等文件, 再新建自定義的.
- 新建模板App;
- 刪除所有AppDelegate/storyboard/ViewController文件, 剩餘Info.plist和main.m文件;
- Info.plist中把key”Launch screen interface file base name”與”Main storyboard file base name”後面的value去掉;
- 新建一個MyAppDelegate(名稱自定)繼承自UIResponder, 實現UIApplicationDelegate協議,
- 在MyAppDelegate.h文件中創建一個UIWindow實例對象myWindow, 使用強引用;
- 新建一個MyViewController繼承自UIViewController;
- 在MyAppDelegate.m中實現代理方法application:didFinishLaunchingWithOptions:, 返回值YES,
- 在main.m導入我們新建的MyAppDelegate.h, 並修改main函數裏面的AppDelegate爲MyAppDelegate;
- 最後在MyAppDelegate.m導入MyViewController.h, 在application:didFinishLaunchingWithOptions:方法中添加如下代碼.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.myWindow = [[UIWindow alloc] init]; // 實例化UIWindow, 其尺寸默認爲屏幕大小([UIScreen mainScreen])
MyViewController *VC = [[MyViewController alloc] init]; // 實例化MyViewController
self.myWindow.rootViewController = VC; // 設置rootViewController, 相當於強引用VC, 故而VC不需用全局變量
[self.myWindow makeKeyAndVisible]; // 設爲keyWindow並使其可見(Hidden=NO)
return YES;
}
以上創建的工程和使用模板創建的工程對比, 除了不使用storyboard, 效果均一致.
如果要銷燬一個UIWindow對象, 可將其置nil即可 (至於網上也有人說先hidden再nil, 但我測試好像沒有這個必要). nil後, keyWindow會自動變爲[UIApplication sharedApplication].windows中level高的並且是可見的window, 如果level相同, 後添加的將成爲新keyWindow; 如果不符合前面的條件, 則keyWindow值爲nil.
到這裏, 我們應該對UIWindow有了個簡單的認識, 接下來再探討一下UIWIndow的一些方法屬性.
方法屬性
@property(nonatomic,strong) UIScreen *screen NS_AVAILABLE_IOS(3_2); // default is [UIScreen mainScreen]. changing the screen may be an expensive operation and should not be done in performance-sensitive code
@property(nonatomic) UIWindowLevel windowLevel; // default = 0.0
@property(nonatomic,readonly,getter=isKeyWindow) BOOL keyWindow;
- (void)becomeKeyWindow; // override point for subclass. Do not call directly
- (void)resignKeyWindow; // override point for subclass. Do not call directly
- (void)makeKeyWindow;
- (void)makeKeyAndVisible; // convenience. most apps call this to show the main window and also make it key. otherwise use view hidden property
@property(nullable, nonatomic,strong) UIViewController *rootViewController NS_AVAILABLE_IOS(4_0); // default is nil
- (void)sendEvent:(UIEvent *)event; // called by UIApplication to dispatch events to views inside the window
- (CGPoint)convertPoint:(CGPoint)point toWindow:(nullable UIWindow *)window; // can be used to convert to another window
- (CGPoint)convertPoint:(CGPoint)point fromWindow:(nullable UIWindow *)window; // pass in nil to mean screen
- (CGRect)convertRect:(CGRect)rect toWindow:(nullable UIWindow *)window;
- (CGRect)convertRect:(CGRect)rect fromWindow:(nullable UIWindow *)window;
這些方法屬性Apple基本都有註解, 下面挑重點一一探討.
1. screen
默認爲[UIScreen mainScreen] (屏幕大小). 我們也可自定義一個尺寸, 但是假如不鋪滿屏幕, 尺寸之外的區域將顯示不出來, 呈黑色.
2. windowLevel
window層級, 表示在z軸方向上的位置關係. 屬於CGFloat類型, 默認值爲0.0, 取值範圍爲-10000000.0到10000000.0. 驗證代碼如下:
self.myWindow.windowLevel = -100000000000000.0;
NSLog(@"%f", self.myWindow.windowLevel); // -10000000.000000
self.myWindow.windowLevel = 100000000000000.0;
NSLog(@"%f", self.myWindow.windowLevel); // 10000000.000000
另外, 系統定義了幾個層級, 其值如下:
UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; // 0.0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; // 2000.0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar; // 1000.0
在可見狀態下, 層級高的window會遮擋掉層級低的window. 例如: 設置myWindow.windowLevel=999.9, 狀態欄可見; myWindow.windowLevel=1000.1, 狀態欄被覆蓋不可見.
如果兩個window的windowLevel相等, 那麼後顯示出來的window會覆蓋前面的, 所謂顯示, 指的是self.myWindow.hidden=No或[self.myWindow makeKeyAndVisible]操作.
3. keyWindow
The key window receives keyboard and other non-touch related events. Only one window at a time may be the key window.
這個屬性相當重要, 因爲一個window只有成爲keyWindow才能接收鍵盤事件和非觸摸類事件. 這裏所說的鍵盤事件, 並不是指鍵盤的彈出收起事件, 因爲這些事件任何一個類只要註冊通知都能接收到, 這裏說的鍵盤事件應該指的是鍵盤傳遞的值. 例如, 創建兩個window同時顯示, 每個window上的view裏面添加一個textField用於彈出鍵盤, 當點擊其中一個textField後, 其所在的window就會被設置成keyWindow, 然後纔可愉快地輸入.
同一時刻只能有一個keyWindow.
這裏順便提一下, 假如一開始我們沒設置makeKeyAndVisible, 而只是hidden=No, 此時window仍可顯示, 但是keyWindow爲nil, 如果在界面上面添加textField, 點擊後系統就會將當前的window設爲keyWindow.
這個屬性是readonly只讀屬性, 如果要設置一個window爲keyWindow, 使用makeKeyWindow或者makeKeyAndVisible方法.
4. becomeKeyWindow
當window成爲keyWindow時, 系統會調用此方法同時發出UIWindowDidBecomeKeyNotification通知, 以便該window知道自己成爲了keyWindow. 需要注意的是, 這是類似一個系統回調方法, 我們不要主動調用他, 否則可能出現不可預料後果.
我們可以重寫它, 來執行成爲keyWindow後的相關任務.
5. resignKeyWindow
當window從keyWindow變成非KeyWindow時, 系統會調用此方法同時發出UIWindowDidResignKeyNotification通知. 同上.
6. makeKeyWindow
調用此方法的window將成爲新的keyWindow, 同時不改變其可見性(hidden). 強調, 同一時刻只能有一個keyWindow.
7. makeKeyAndVisible
調用此方法的window將成爲新的keyWindow, 同時可見(hidden=NO). 相當於 makeKeyWindow + hidden=NO.
8. rootViewController
根視圖控制器, 提供窗口的內容視圖, 即rootViewController的self.view當做window的內容視圖來展示, 其view跟隨window的大小的變化而變化.
9. sendEvent:
UIApplication對象調用這個方法分派事件給window, 然後window又將這些事件分派給適當的view (UIApplication和UIWindow均聲明瞭sendEvent:方法). 我們可以自定義子類繼承自UIApplication/UIWindow, 並重寫這個方法, 將事件分派給UIResponder的響應程序鏈, 實現對事件的監控或執行特殊的事件處理.
10. convertPoint…
座標轉換. 略.
window變化通知
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeVisibleNotification; // 當window顯示(hidden=NO)
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeHiddenNotification; // 當window隱藏(hidden=YES)
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeKeyNotification; // 當window成爲keyWindow
UIKIT_EXTERN NSNotificationName const UIWindowDidResignKeyNotification; // 當window變成非keyWindow
注意:
- 如果當前keyWindow直接nil, 不會發送UIWindowDidResignKeyNotification通知; 而只有別的window調用makeKeyWindow/makeKeyAndVisible後, 纔會發送UIWindowDidResignKeyNotification通知. 例如: 有兩個window A和B, A爲keyWindow, 當直接window A = nil, 則keyWindow變成window B, 但此時不會發送通知; 而A爲keyWindow, B調用makeKeyWindow/makeKeyAndVisible後, 則B也會變成keyWindow, 同時發送通知.
- 如果對當前keyWindow直接隱藏(hidden=YES), 首先會發送UIWindowDidResignKeyNotification通知並且辭去keyWindow職務, 接着發送UIWindowDidBecomeHiddenNotification通知.
應用
這個類在iOS9.0中被遺棄, 但目前還可使用. 當我們show這個類的實例 (彈出提示框) 的時候, 系統實際上先後創建了兩個window: _UIAlertControllerShimPresenterWindow 和 UITextEffectsWindow.
show流程是:
- _UIAlertControllerShimPresenterWindow可見
- 當前keyWindow變成非keyWindow
- _UIAlertControllerShimPresenterWindow成爲keyWindow
- UITextEffectsWindow可見
dismiss流程是:
- _UIAlertControllerShimPresenterWindow變成非keyWindow
- 原來的window成爲keyWindow
- _UIAlertControllerShimPresenterWindow隱藏
在alertView出來的時候, keyWindow是_UIAlertControllerShimPresenterWindow; alertView消失後, windows中仍保留UITextEffectsWindow.
_UIAlertControllerShimPresenterWindow是什麼?
暫時查不到資料, 但根據字面意思, 是alaerView的呈現載體, 也就是說alertView在_UIAlertControllerShimPresenterWindow裏面, 而後者不被App持有.
UITextEffectsWindow又是什麼鬼?
也沒啥資料, 大概是和鍵盤輸入相關的window. (alertView的按鈕不就相當於鍵盤嘛…)
iOS9.0之後, 系統建議我們使用的類. 但是和UIAlertView不同的是, 使用UIAlertController彈出彈框後, 雖然增加了UITextEffectsWindow, 但是keyWindow並沒有改變, 而且UIAlertController顯示的view是添加到keyWindow去顯示的.
iOS10.0之前, 調用系統鍵盤後, 出現UITextEffectsWindow; iOS10.0之後, 新增了UIRemoteKeyboardWindow.
UITextEffectsWindow是和鍵盤輸入相關的window
UIRemoteKeyboardWindow是鍵盤視圖所在的window, 而且level爲最高的10000000.0
當我們使用支付寶或者一些金融類App的時候, 會發現當App從後臺返回前臺的時候, 總是要重新輸入密碼, 以提高軟件安全性. 這個重新輸入的密碼界面一般是通過新增一個window來實現的, 因爲它可能從App中任意一個界面調用出來, 使用VC或者view的話終究不太方便.
下面來簡單介紹一下實現過程, 代碼比較簡單, 就不貼上了:
- AppDelegate中監聽返回前臺的通知applicationWillEnterForegroundNotification;
- 在通知中實例化一個自定義window並makeKeyAndVisible, window中加入我們的密碼界面;
- 當用戶輸入正確密碼後直接將該window=nil, 系統自動將keyWindow變爲上之前的window.
只是簡單實現, 拒絕和那些成熟的項目作對比.
工程結構如下:
在模板App上面添加了兩個VC, 分別作爲suspensionWindow和logWindow的rootViewController. 下面直接貼上代碼.
AppDelegate.h
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) UIWindow *suspensionWindow;
@property (nonatomic, strong) UIWindow *logWindow;
@end
AppDelegate.m
#import "AppDelegate.h"
#import "SuspensionViewController.h"
#import "LogViewController.h"
@interface AppDelegate () <SuspensionViewControllerDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 創建suspensionWindow
self.suspensionWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 100, 60, 60)];
SuspensionViewController *suspensionVC = [[SuspensionViewController alloc] init];
suspensionVC.delegate = self;
self.suspensionWindow.rootViewController = suspensionVC;
self.suspensionWindow.windowLevel = 0.2; // 主window 0.0, 鍵盤window 1.0, logWindow 0.1
self.suspensionWindow.hidden = NO; // 可見
return YES;
}
#pragma makr - SuspensionViewControllerDelegate
// 單擊懸浮球 回調
- (void)suspensionViewController:(SuspensionViewController *)suspensionVC didClickButton:(UIButton *)btn {
if (btn.selected) {
// 創建logWindow並設爲可見
self.logWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
LogViewController *logVC = [[LogViewController alloc] init];
self.logWindow.rootViewController = logVC;
self.logWindow.windowLevel = 0.1;
self.logWindow.hidden = NO;
}else {
// 銷燬logWindow
self.logWindow = nil;
}
}
@end
ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeIsUp:) userInfo:nil repeats:YES];
}
- (void)timeIsUp:(NSTimer *)timer {
NSString *message = [NSString stringWithFormat:@"%@ test", [self getCurrentTimeString]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"LogMessageNotification" object:message];
}
// 獲取當前時間str
- (NSString *)getCurrentTimeString {
// 日期解析器
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.timeZone = [NSTimeZone systemTimeZone]; // 設置時區 (跟隨系統)
[dateFormatter setDateFormat:@"hh:mm:ss:SSS"]; // 設置時間字符串格式
// 獲取當前時間(GMT)
NSDate *date = [NSDate date];
// 轉換成時間字符串
NSString *dateStr = [dateFormatter stringFromDate:date];
return dateStr;
}
@end
SuspensionViewController.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class SuspensionViewController;
@protocol SuspensionViewControllerDelegate <NSObject>
@optional
- (void)suspensionViewController:(SuspensionViewController *)suspensionVC didClickButton:(UIButton *)btn;
@end
@interface SuspensionViewController : UIViewController
@property (nonatomic, weak) id<SuspensionViewControllerDelegate> delegate;
@end
NS_ASSUME_NONNULL_END
SuspensionViewController.m
#import "SuspensionViewController.h"
#import "AppDelegate.h"
@interface SuspensionViewController ()
@property (nonatomic, strong) UIButton *btn;
@end
@implementation SuspensionViewController
- (void)viewDidLoad {
[super viewDidLoad];
// init UI
self.view.backgroundColor = [UIColor purpleColor];
self.view.alpha = 0.5;
[self.view addSubview:self.btn];
// 添加拖動手勢
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self.view addGestureRecognizer:pan];
}
- (void)viewWillLayoutSubviews {
NSLog(@"%s", __func__);
self.view.layer.cornerRadius = self.view.frame.size.width/2.0;
self.btn.frame = self.view.bounds;
}
#pragma mark - UI事件
- (void)btnAction:(UIButton *)btn {
btn.selected = !btn.selected;
if ([self.delegate respondsToSelector:@selector(suspensionViewController:didClickButton:)]) {
[self.delegate suspensionViewController:self didClickButton:btn];
}
}
- (void)panAction:(UIPanGestureRecognizer *)sender {
// 獲取AppDelegate實例, 主window, 懸浮球window
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
UIWindow *mainWindow = appDelegate.window;
UIWindow *suspensionWindow = appDelegate.suspensionWindow;
switch (sender.state) {
case UIGestureRecognizerStateBegan:
{
self.view.alpha = 1.0;
}
break;
case UIGestureRecognizerStateChanged:
{
// 懸浮球跟隨手勢移動
suspensionWindow.center = [sender locationInView:mainWindow];
}
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateFailed:
case UIGestureRecognizerStateCancelled:
{
self.view.alpha = 0.5;
// 懸浮球靠邊
float x = suspensionWindow.frame.size.width/2;
if (suspensionWindow.center.x > mainWindow.frame.size.width/2) {
x = mainWindow.frame.size.width - suspensionWindow.frame.size.width/2;
}
[UIView animateWithDuration:0.2 animations:^{
suspensionWindow.center = CGPointMake(x, suspensionWindow.center.y);
}];
}
break;
default:
break;
}
}
#pragma mark - lazy
- (UIButton *)btn {
if (_btn == nil) {
_btn = [UIButton buttonWithType:UIButtonTypeCustom];
_btn.frame = self.view.bounds;
[_btn setTitle:@"Log" forState:UIControlStateNormal];
[_btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}
return _btn;
}
@end
LogViewController.m
#import "LogViewController.h"
@interface LogViewController ()
@property (nonatomic, strong) UITextView *textView;
@end
@implementation LogViewController
- (void)viewDidLoad {
[super viewDidLoad];
// init UI
self.view.backgroundColor = [UIColor colorWithRed:177.0/255.0 green:177.0/255.0 blue:177.0/255.0 alpha:0.5];
[self.view addSubview:self.textView];
// 監聽通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(logMessage:) name:@"LogMessageNotification" object:nil];
}
- (void)dealloc
{
// 移除所有通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 通知響應方法
- (void)logMessage:(NSNotification *)sender {
NSString *message = (NSString *)sender.object;
dispatch_async(dispatch_get_main_queue(), ^{
self.textView.text = [NSString stringWithFormat:@"%@%@\n", self.textView.text, message];
// 總是跳到最後一行
[self.textView scrollRangeToVisible:NSMakeRange(self.textView.text.length, 1)];
});
}
#pragma mark - lazy
- (UITextView *)textView {
if (_textView == nil) {
_textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2)];
_textView.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2);
_textView.backgroundColor = [UIColor colorWithRed:0 green:1.0 blue:1.0 alpha:0.5];
_textView.editable = NO; // 禁用鍵盤
_textView.text = @"";
}
return _textView;
}
@end
運行效果如下: