UIWindow in iOS

原文地址 UIWindow in iOS

在这篇文章里,我将要分享一些我对于 UIWindow 的了解。

keyWindow

一个 app 可以有许多个 UIWindow。key window 是指定的被用来接收键盘和其他非触摸事件。在一个时刻内只有一个窗口的话那很可能就是 key window。

你可以调用 makeKeyAndVisiblemakeKeyWindow 方法来使一个 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

当设备旋转的时候,系统会发布 UIApplicationWillChangeStatusBarFrameNotificationUIApplicationDidChangeStatusBarFrameNotification 通知。

让我们现在来处理 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 考虑在内。留意 infoPlistSupportedInterfaceOrientationsMaskviewControllerForStatusBarAndOrientationProperties,你能从那里了解到许多关键的东西。

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。

所有的这些都能够返回它们当前的特征集合给你,用来决定你的界面应该如何布局。

参考

  1. Multiple UIWindows
  2. To create a new UIWindow over the main window
  3. Advantages, problems, examples of adding another UIWindow to an iOS app?
  4. The difference between a UIWindow and a UIView
  5. Using multiple UIWindows in iOS applications
  6. Technical Q&A QA1688 Why won’t my UIViewController rotate with the device?
  7. After rotation UIView coordinates are swapped but UIWindow’s are not?