iOS开发之UI篇(12)—— UIWindow

版本
Xcode 10.2
iPhone 6s (iOS12.4)
( 本文示例所用测试版本如上, 一些方法结论可能不适用于较旧版本的iOS/Xcode, 如需使用应先测试验证. )

目录

UIWindow继承
简单结构图
简介
一些常见的window
window的创建过程
属性方法
应用

继承关系

UIWindow : UIView : UIResponder : NSObject

结构

新建一个”Single View App”模板App, 点击Debug view hierarchy按钮, 打开的层级关系图如下:

iOS开发之UI篇(12)—— UIWindow_第1张图片
UIWindow简单结构

简介

The backdrop for your app’s user interface and the object that dispatches events to your views.
编者译: UIWindow对象充当了App中UI界面的背景(容器/载体), 还有一个作用是分派事件给各种view.

根据Apple官方文档的这段话, 可以延伸为以下两点:

  1. App中任何一个view, 只有添加到相应的window中, 才能显示出来;
  2. 触摸事件会被传递到触摸区域内的最上层的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. 大概流程是:

  1. 程序入口main(),
  2. 调用UIApplicationMain方法创建UIApplication对象(默认为AppDelegate),
  3. 根据Info.plist里”Main storyboard file base name” (等同targets中的Main interface选项) 对应的名称作为main storyboard并加载之,
  4. 偷偷摸摸地实例化AppDelegate.h中的window,
  5. 将Main.storyboard里的Initial View Controller (默认为ViewController)设为window的rootViewController, 此时相当于把ViewController的View添加到window中,
  6. 执行makeKeyAndVisible方法将window设为keyWindow并使其可见(hidden=NO),
  7. 创建完成, 显示.
  • 自定义创建

本来想新建一个空的project来从头演示window的创建, 但是新版Xcode默认不允许创建空的项目. 无奈只好先创建模板App, 然后删除默认storyboard/AppDelegate等文件, 再新建自定义的.

  1. 新建模板App;
  2. 删除所有AppDelegate/storyboard/ViewController文件, 剩余Info.plist和main.m文件;
  3. Info.plist中把key”Launch screen interface file base name”与”Main storyboard file base name”后面的value去掉;
  4. 新建一个MyAppDelegate(名称自定)继承自UIResponder, 实现UIApplicationDelegate协议,
  5. 在MyAppDelegate.h文件中创建一个UIWindow实例对象myWindow, 使用强引用;
  6. 新建一个MyViewController继承自UIViewController;
  7. 在MyAppDelegate.m中实现代理方法application:didFinishLaunchingWithOptions:, 返回值YES,
  8. 在main.m导入我们新建的MyAppDelegate.h, 并修改main函数里面的AppDelegate为MyAppDelegate;
  9. 最后在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

注意:

  1. 如果当前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, 同时发送通知.
  2. 如果对当前keyWindow直接隐藏(hidden=YES), 首先会发送UIWindowDidResignKeyNotification通知并且辞去keyWindow职务, 接着发送UIWindowDidBecomeHiddenNotification通知.

应用

  • UIAlertView

这个类在iOS9.0中被遗弃, 但目前还可使用. 当我们show这个类的实例 (弹出提示框) 的时候, 系统实际上先后创建了两个window: _UIAlertControllerShimPresenterWindowUITextEffectsWindow.

show流程是:

  1. _UIAlertControllerShimPresenterWindow可见
  2. 当前keyWindow变成非keyWindow
  3. _UIAlertControllerShimPresenterWindow成为keyWindow
  4. UITextEffectsWindow可见

dismiss流程是:

  1. _UIAlertControllerShimPresenterWindow变成非keyWindow
  2. 原来的window成为keyWindow
  3. _UIAlertControllerShimPresenterWindow隐藏

在alertView出来的时候, keyWindow是_UIAlertControllerShimPresenterWindow; alertView消失后, windows中仍保留UITextEffectsWindow.

_UIAlertControllerShimPresenterWindow是什么?
暂时查不到资料, 但根据字面意思, 是alaerView的呈现载体, 也就是说alertView在_UIAlertControllerShimPresenterWindow里面, 而后者不被App持有.

UITextEffectsWindow又是什么鬼?
也没啥资料, 大概是和键盘输入相关的window. (alertView的按钮不就相当于键盘嘛..)

  • UIAlertController

iOS9.0之后, 系统建议我们使用的类. 但是和UIAlertView不同的是, 使用UIAlertController弹出弹框后, 虽然增加了UITextEffectsWindow, 但是keyWindow并没有改变, 而且UIAlertController显示的view是添加到keyWindow去显示的.

  • keyboard

iOS10.0之前, 调用系统键盘后, 出现UITextEffectsWindow; iOS10.0之后, 新增了UIRemoteKeyboardWindow. UITextEffectsWindow是和键盘输入相关的window
UIRemoteKeyboardWindow是键盘视图所在的window, 而且level为最高的10000000.0

  • 重登陆

当我们使用支付宝或者一些金融类App的时候, 会发现当App从后台返回前台的时候, 总是要重新输入密码, 以提高软件安全性. 这个重新输入的密码界面一般是通过新增一个window来实现的, 因为它可能从App中任意一个界面调用出来, 使用VC或者view的话终究不太方便.
下面来简单介绍一下实现过程, 代码比较简单, 就不贴上了:

  1. AppDelegate中监听返回前台的通知applicationWillEnterForegroundNotification;
  2. 在通知中实例化一个自定义window并makeKeyAndVisible, window中加入我们的密码界面;
  3. 当用户输入正确密码后直接将该window=nil, 系统自动将keyWindow变为上之前的window.
  • 悬浮球

只是简单实现, 拒绝和那些成熟的项目作对比.
工程结构如下:


iOS开发之UI篇(12)—— UIWindow_第2张图片
悬浮球Demo

在模板App上面添加了两个VC, 分别作为suspensionWindow和logWindow的rootViewController. 下面直接贴上代码.

AppDelegate.h

#import 

@interface AppDelegate : UIResponder 

@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 () 

@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 

NS_ASSUME_NONNULL_BEGIN

@class SuspensionViewController;

@protocol SuspensionViewControllerDelegate 

@optional
- (void)suspensionViewController:(SuspensionViewController *)suspensionVC didClickButton:(UIButton *)btn;

@end


@interface SuspensionViewController : UIViewController

@property (nonatomic, weak) id    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

运行效果如下:


iOS开发之UI篇(12)—— UIWindow_第3张图片
LearnUIWindow

你可能感兴趣的:(iOS开发之UI篇(12)—— UIWindow)