原文地址 UIWindow in iOS
在这篇文章里,我将要分享一些我对于 UIWindow 的了解。
keyWindow
一个 app 可以有许多个 UIWindow
。key window 是指定的被用来接收键盘和其他非触摸事件。在一个时刻内只有一个窗口的话那很可能就是 key window。
你可以调用 makeKeyAndVisible
或 makeKeyWindow
方法来使一个 UIWindow
成为 keyWindow
。注意,UIWindow
默认是隐藏的,因此 makeKeyAndVisible
不仅能够让一个 UIWindow
成为 keyWindow
还可会同时设置它的 hidden
属性为 NO
。
UIWindow 总是 portrait 方向
仅向 application:didFinishLaunchingWithOptions:
增加以下代码:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor greenColor];
view.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
[self.window addSubview:view];
view.layer.zPosition = FLT_MAX;
然后旋转设备/模拟器,你将会看到绿色视图总是在 (0, 0) 坐标处。
UIWindow 的坐标系统总是处在 portrait 方向。它通过设置 rootViewController
的视图的 transform
属性来应用旋转。因此从 iOS 4 系统开始,Apple 建议应用程序使用 rootViewController
。请查看来自 rob 的 After rotation UIView coordinates are swapped but UIWindow’s are not? 这个问题精彩的回答。
Keyboard is a window
注意,UIApplication
有一个只读属性叫 windows
。
app 可见和隐藏的 window。(read-only)
这个属性包含有 app 当前所有相关联的 UIWindow 对象。这个数组不包括由系统创建和管理的 window,例如那些用来显示 status bar 的 window。
你可以遍历这个 window 数组来得到 keyboard 的 window。从 SVProgressHUD 提取到的代码。注意,当 keyboard 显示出来的时候,window 数组会包含有另一种 UITextEffectWindow
类型的 window,这个就是 keyboard。
- (CGFloat)visibleKeyboardHeight {
UIWindow *keyboardWindow = nil;
for (UIWindow *testWindow in [[UIApplication sharedApplication] windows]) {
if(![[testWindow class] isEqual:[UIWindow class]]) {
keyboardWindow = testWindow;
break;
}
}
for (__strong UIView *possibleKeyboard in [keyboardWindow subviews]) {
if([possibleKeyboard isKindOfClass:NSClassFromString(@"UIPeripheralHostView")] || [possibleKeyboard isKindOfClass:NSClassFromString(@"UIKeyboard")])
return possibleKeyboard.bounds.size.height;
}
正如我之前说的,UIWindow 的坐标系总是处在 portrait 方向。keyboard 也是一个 window,因此它的 frame
总是 {(0, 0), (320, 480)}(译者注:此为 iPhone 设备),并且与设备方向无关。当你旋转设备的时候,系统会给 keyboard window 应用一次旋转变换。
用户旋转设备到 portrait 方向时:
<UITextEffectsWindow: 0x8e3ecc0; frame = (0 0; 320 480); opaque = NO; gestureRecognizers = <NSArray: 0x8e3f240>; layer = <UIWindowLayer: 0x8e3ee40>>
用户旋转设备到 landscape 方向时:
<UITextEffectsWindow: 0x8e3ecc0; frame = (0 0; 320 480); transform = [0, 1, -1, 0
Notification
你可以通过以下代码验证是否已经注册了 UIKeyboardWillShowNotification
通知:
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification object:nil queue:nil usingBlock:^(NSNotification *notification) {
NSLog(@"%@", notification);
}];
用户旋转设备到 portrait 方向时:
UIKeyboardFrameEndUserInfoKey = “NSRect: {{0, 264}, {320, 216}}”;
用户旋转设备到 landscape 方向时:
UIKeyboardFrameEndUserInfoKey = “NSRect: {{0, 0}, {162, 480}}”;
因此,在你的 UIKeyboardWillShowNotification 处理程序中,你应该转换 keyboard 为你 view 的坐标系统。从 Keyboard “WillShow” and “WillHide” vs. Rotation 这个答案中提取的代码:
- (void)keyboardWillShow:(NSNotification *)aNotification
{
CGRect keyboardFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect convertedFrame = [self.view convertRect:keyboardFrame fromView:self.view.window];
......
/* Do whatever you want now with the new frame.
* The width and height will actually be correct now
*/
......
}
Status bar is a window
正如之前所说的,windows 数组不包含有 statusbar window。Statusbar window 是 UIStatusBarWindow
类型,你可以用下面代码(来自 FLEX)获得这个 window。
- (UIWindow *)statusWindow
{
NSString *statusBarString = [NSString stringWithFormat:@"_statusBarWindow"];
return [[UIApplication sharedApplication] valueForKey:statusBarString];
}
跟 keyboard window 类似,statusbar frame 同样不会改变。当你旋转设备的时候,系统会对 statusbar 应用旋转变换。
用户旋转设备到 portrait:
<UIStatusBarWindow: 0x8f61e30; frame = (0 0; 320 480); gestureRecognizers = <NSArray: 0x8f62e40>; layer = <UIWindowLayer: 0x8f620d0>>
用户旋转设备到 landscape:
<UIStatusBarWindow: 0x8f61e30; frame = (0 0; 320 480); transform = [0, 1, -1, 0, 0, 0]; gestureRecognizers = <NSArray: 0x8f62e40>; layer = <UIWindowLayer: 0x8f620d0>>
statusBarFrame
UIApplication
有一个属性叫 statusBarFrame
,它总是处在 portrait 坐标系统内。
CGRect rect = [UIApplication sharedApplication].statusBarFrame;
NSLog(@"statusBarFrame %@", NSStringFromCGRect(rect));
用户旋转设备到 portrait
statusBarFrame {{0, 0}, {320, 20}}
用户旋转设备到 landscape
statusBarFrame {{300, 0}, {20, 480}}
如果你只想知道 status 高度, 而不关心屏幕方向的话,使用下面的代码能够获得最大高度。
float statusBarHeight = MIN([UIApplication sharedApplication].statusBarFrame.size.height, [UIApplication sharedApplication].statusBarFrame.size.width);
Notification
当设备旋转的时候,系统会发布 UIApplicationWillChangeStatusBarFrameNotification
和 UIApplicationDidChangeStatusBarFrameNotification
通知。
让我们现在来处理 UIApplicationDidChangeStatusBarFrameNotification
看看能得到什么:
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillChangeStatusBarFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
NSLog(@"%@", note);
}];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidChangeStatusBarFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
NSLog(@"%@", note);
}];
我不知道为什么,但当 DidChange
通知给我过时的结果时, WillChange
通知却给了我最终想要的结果。由结果可知,我的意思是通过 statusBarFrame
的值我同样看到没有函数用到了 DidChange
通知。
How to create alert view
你可以在出现一个 UIViewController
的时候创建一个 alertView。对于高级用法,你必须理解呈现时的上下文。这种使用 alertView 的方式有点难。而我们总会简单调用这句话:
[alertView show];
这不代表随便什么 view 都能显示在 alertView 的上面,因为这会 alertView 的本意。
非常有想象的方式就是使用到 UIWindow
。我看到许多库会用到这两种方式中的其中一种。
Don’t use rootViewController approach
有些库,像 OLGhostAlertView,SVProgressHUD,WYPopoverController,MTStatusBarOverlay...不会使用 rootViewController
。它们会创建一个新的 UIWindow
(MTStatusBarOverlay)或者使用已存在的 UIWindow
,它们通过 addSubview
方法直接添加到 window 上,因此它们通过监听 UIApplicationDidChangeStatusBarOrientationNotification
或者 UIApplicationWillChangeStatusBarFrameNotification
来处理方向问题。
处理方法一般类似于下面这种(来自 SVProgressHUD)。这种思路通过手动的获得方向并给他们的 view 应用旋转变换。
- (void)handleOrientationChange:(NSNotification *)note
{
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
CGRect orientationFrame = [UIScreen mainScreen].bounds;
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
if(UIInterfaceOrientationIsLandscape(orientation)) {
float temp = orientationFrame.size.width;
orientationFrame.size.width = orientationFrame.size.height;
orientationFrame.size.height = temp;
temp = statusBarFrame.size.width;
statusBarFrame.size.width = statusBarFrame.size.height;
statusBarFrame.size.height = temp;
}
switch (orientation) {
case UIInterfaceOrientationPortraitUpsideDown:
rotateAngle = M_PI;
break;
case UIInterfaceOrientationLandscapeLeft:
rotateAngle = -M_PI/2.0f;
break;
case UIInterfaceOrientationLandscapeRight:
rotateAngle = M_PI/2.0f;
break;
default: // as UIInterfaceOrientationPortrait
rotateAngle = 0.0;
break;
}
}
How to add view to existing window
这些库有它们自己的方式来添加 view 到已存在的 window。
SVProgressHUD 通过这种方式来添加 view 到 window 的顶层。
if(!self.overlayView.superview){
NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication] windows] reverseObjectEnumerator];
for (UIWindow *window in frontToBackWindows)
if (window.windowLevel == UIWindowLevelNormal) {
[window addSubview:self.overlayView];
break;
}
}
How the OS creates AlertView
观看 WWDC 2014 Session 228 A Look Inside Presentation Controllers, 里面说到
在幕后,framework 创建了一个代表你 app 的窗口,但这个 window 优先于 iOS 8 window 的旋转行为,因此这个 window 从技术上来说仍然是 portrait 方向。
然后我们向那个 window 添加了动作表并且模仿已经出现的 view 的变换层次来得到正确的方向。
Use rootViewController approach
像 FLEX,SIAlertView,...都是通过创建一个新的 UIWindow
并分配 rootViewController
给那个 window。这种方式的方向由 rootViewController
决定。这些开源库仅简单地给 rootViewController
添加了子视图。在视图控制器内查看和处理视图方向。
从 FLEXViewExplorerViewController 提取的代码:
- (NSUInteger)supportedInterfaceOrientations
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
NSUInteger supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
if (viewControllerToAsk && viewControllerToAsk != self) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
// The UIViewController docs state that this method must not return zero.
// If we weren't able to get a valid value for the supported interface orientations, default to all supported.
if (supportedOrientations == 0) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
return supportedOrientations;
}
- (BOOL)shouldAutorotate
{
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
}
return shouldAutorotate;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[...]
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[...]
}
既然 FLEX 创建了一个新的 window,那它在处理旋转功能的时候必须把 app 的原始 window 考虑在内。留意 infoPlistSupportedInterfaceOrientationsMask
和 viewControllerForStatusBarAndOrientationProperties
,你能从那里了解到许多关键的东西。
UIWindow is a UIView
既然 UIWindow 是一个 UIView 子类,那你就能够做许多已知的 UIView 操作。例如,FLEX 重写了 pointInside:withEvent:
通过它的 toolbar 来截获触摸事件。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
}
return pointInside;
}
UIWindow in iOS 8
在 iOS 8,Apple 介绍了 Size Classes,还有 「旋转是一种 bouds 变换的动画」。并且 UIWindow 的方向与设备方向一致。
WWDC 2014 Session 216 使用 UIKit 构建自适应的 App
UITraitEnvironment
是一种新的协议,它能够返回它们当前的特征集合,并且这些集合包含有 Screens,Windows,View Controllers,还有 Views。
所有的这些都能够返回它们当前的特征集合给你,用来决定你的界面应该如何布局。
参考
- Multiple UIWindows
- To create a new UIWindow over the main window
- Advantages, problems, examples of adding another UIWindow to an iOS app?
- The difference between a UIWindow and a UIView
- Using multiple UIWindows in iOS applications
- Technical Q&A QA1688 Why won’t my UIViewController rotate with the device?
- After rotation UIView coordinates are swapped but UIWindow’s are not?