系统化学习,知其然,知其所以然
每个 iOS 应用程序至少需要一个窗口, 有些可能包含多个窗口。一个窗口对象有几个职责:
- 作为一个容器包含应用程序的可见内容。
- 将触摸事件传递到视图和其他对象过程中起着关键作用。
- 和视图控制器协作,处理设备旋转方向。
在iOS中,Windows没有标题栏,关闭框或其他视觉装饰。一个窗口始终只是一个或多个视图的空白容器。此外,应用程序不会通过显示新窗口来更改其内容。如果要更改显示的内容,请改为改变窗口的最前面的视图。
大多数iOS应用程序在其生命周期中只创建并使用一个窗口。该窗口占据设备的整个主屏幕,并在应用程序生命周期的早期从应用程序的主要 storyboard/nib 文件(或以编程方式创建)加载。但是,如果应用程序支持使用外部显示器进行视频输出,则可以创建一个额外的窗口来在该外部显示器上显示内容。所有其他窗口通常由系统创建,通常是为了响应特定事件(例如来电)创建的。
一、关于Windows的作用
对于许多应用程序,APP 与窗口交互的唯一时间是在启动时创建窗口的时间。 但是,可以使用窗口对象来执行其他任务:
进行坐标转换。
使用窗口通知来跟踪与窗口相关的更改。 Windows会在显示或隐藏通知或者接受或退出密钥状态时生成通知。 您可以使用这些通知在应用程序的其他部分执行操作。 有关更多信息,请参阅监视窗口更改。
二、Window的创建和设置
创建Window有两种方式: 编程方式和Interface Builder。有以下注意点
- 在启动时创建窗口,保留该窗口并将其引用存储在AppDelegate中。
- 如果创建额外的窗口,让应用程序在需要时创建它们。 例如,支持在外部显示器上显示内容,则应在显示器连接成功后创建相应窗口。
- 无论应用程序是启动到前台还是后台,都应始终在启动时创建应用程序的主窗口。
- 如果应用程序直接进入后台,则应避免在应用程序进入前台之前显示窗口。
2.1 创建方式一:Interface Builder
分为2种使用场景:新工程和旧工程
- 新建工程,Xcode的工程模板会自动完成创建工作,文件命为 Main.storyboard(不同时期名称不同)。
- 从代码迁移到 Interface Builder ,除了拖拽一个 Window 之外,还需要完成以下操作
- 想在运行时访问,一般做法是创建一个引用保存到 AppDelegate 对象中
- 修改 Info.plist 文件中 UIMainStoryboardFile 的值,以便AppDelegate调用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
时,Main.storyboard 已经加载完毕。
2.2 创建方式二:代码创建
//首先,在AppDelegate中创建一个引用,方便运行时访问
@property (strong, nonatomic) UIWindow *window;
//然后创建对象
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//注意一定要全屏尺寸
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] ;
return YES;
}
2.3 添加内容
通过
- (void)addSubview:(UIView *)view;
添加view,为了方便更换显示内容,使用一个rootView,其他view添加到rootView,更换显示内容直接更换rootView即可,记得适配设备屏幕尺寸。
此外,Window 提供了一个rootViewController属性
@property(nonatomic, strong) UIViewController *rootViewController;
UIKit 自动使用其view作为 window 的 rootView ,并且自动适配设备屏幕尺寸。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] ;
ViewController *rootViewController = [[ViewController alloc] init];
self.window.rootViewController = rootViewController;
return YES;
}
三、监测 Window 状态变化
可以通过以下通知来追踪状态变化
//显示
UIWindowDidBecomeVisibleNotification
//隐藏
UIWindowDidBecomeHiddenNotification
//关键窗口
UIWindowDidBecomeKeyNotification
//非关键窗口
UIWindowDidResignKeyNotification
显示或隐藏一个窗口,传递UIWindowDidBecomeVisibleNotification和UIWindowDidBecomeHiddenNotification通知。当应用程序进入后台执行状态时,不会传递这些通知。即使窗口在应用程序处于后台时并未显示在屏幕上,但在应用程序的上下文中,它仍被视为可见。
UIWindowDidBecomeKeyNotification和UIWindowDidResignKeyNotification通知帮助识别哪个窗口是关键窗口(当前正在接收键盘事件和其他非触摸相关的事件)。触摸事件传递到发生触摸的窗口,而没有相关坐标值的事件被传递到的关键窗口。KeyWindow 只有一个。
四、在外部显示器上显示内容(Displaying Content on an External Display)
要在外部显示器上显示内容,必须为应用程序创建一个额外的窗口,并将其与代表外部显示器的屏幕对象相关联。一旦窗口与正确的屏幕相关联,您可以添加视图并显示它,就像您为应用程序的主屏幕一样。
UIScreen维护一个代表可用硬件显示的屏幕对象列表。通常,只有一个屏幕对象表示任何基于iOS的设备的主显示屏,但是支持连接到外部显示屏的设备可以具有可用的附加屏幕对象。支持外部显示的设备包括具有Retina显示屏和iPad的iPhone和iPod touch设备。较旧的设备(如iPhone 3GS)不支持外部显示器。
注意:由于外部显示本质上是一个视频输出连接,因此没有触摸事件。另外,应用程序负责根据需要更新窗口的内容。因此,要镜像主窗口的内容,应用程序需要为外部显示器的窗口创建一个重复的视图,并保持同步更新。
可以使用 AirServer 软件在 Mac 电脑上模拟一个支持 AirPlay 的屏幕
基本过程如下
在应用程序启动时,注册屏幕连接和断开通知。
-
当在外部显示器上显示内容时,请创建并配置一个窗口。
- 通过UIScreen的
screens
属性获取外部显示的屏幕对象。 - 创建一个UIWindow对象,并根据屏幕(或内容)适当地调整它的大小
- 将外部显示的UIScreen对象分配给窗口的
screen
属性 - 根据需要调整屏幕对象的分辨率以支持您的内容
- 通过UIScreen的
显示并正常更新窗口。
4.1 处理屏幕连接和断开通知(Handling Screen Connection and Disconnection Notifications)
@interface ViewController ()
{
UIWindow *_secondWindow;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
[self setupScreenConnectionNotificationHandlers];
}
//注册通知
- (void)setupScreenConnectionNotificationHandlers
{
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
//已连接
[center addObserver:self selector:@selector(handleScreenConnectNotification:)
name:UIScreenDidConnectNotification object:nil];
//已断开
[center addObserver:self selector:@selector(handleScreenDisconnectNotification:)
name:UIScreenDidDisconnectNotification object:nil];
}
- (void)handleScreenConnectNotification:(NSNotification*)aNotification
{
UIScreen* newScreen = [aNotification object];
CGRect screenBounds = newScreen.bounds;
if (!_secondWindow)
{
_secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
_secondWindow.screen = newScreen;
// 初始化 UI
[viewController displaySelectionInSecondaryWindow:_secondWindow];
}
}
- (void)handleScreenDisconnectNotification:(NSNotification*)aNotification
{
if (_secondWindow)
{
// 隐藏并移除window.
_secondWindow.hidden = YES;
_secondWindow = nil;
// 同步主屏幕和外接屏幕显示内容
[viewController displaySelectionOnMainScreen];
}
}
4.2 配置外部显示器的窗口(Configuring a Window for an External Display)
- (void)checkForExistingScreenAndInitializeIfPresent
{
if ([[UIScreen screens] count] > 1)
{
// 关联 window 和 second screen.
// 主屏幕 index 0.
UIScreen* secondScreen = [[UIScreen screens] objectAtIndex:1];
CGRect screenBounds = secondScreen.bounds;
_secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
_secondWindow.screen = secondScreen;
// 添加一个 rootView 到 window
UIView* whiteField = [[UIView alloc] initWithFrame:screenBounds];
whiteField.backgroundColor = [UIColor whiteColor];
[_secondWindow addSubview:whiteField];
// 中间放一个 label.
NSString* noContentString = [NSString stringWithFormat:@"<no content>"];
CGSize stringSize = [noContentString sizeWithFont:[UIFont systemFontOfSize:18]];
CGRect labelSize = CGRectMake((screenBounds.size.width - stringSize.width) / 2.0,
(screenBounds.size.height - stringSize.height) / 2.0,
stringSize.width, stringSize.height);
UILabel* noContentLabel = [[UILabel alloc] initWithFrame:labelSize];
noContentLabel.text = noContentString;
noContentLabel.font = [UIFont systemFontOfSize:18];
[whiteField addSubview:noContentLabel];
// 显示 window.
_secondWindow.hidden = NO;
}
}
4.3 配置外部显示器的屏幕分辨率(Configuring the Screen Mode of an External Display)
根据显示内容,可以在将窗口与屏幕关联之前更改屏幕分辨率。 许多屏幕支持多种分辨率,使用不同的长宽比。屏幕支持的分辨率可以从 UIScreen 的 availableModes 属性获取屏幕支持的分辨率列表
@property(nonatomic, readonly, copy) NSArray<UIScreenMode *> *availableModes;
选择合适的分辨率然后设置
@property(nonatomic, strong) UIScreenMode *currentMode;
有关屏幕分辨率的更多信息,请参阅UIScreenMode参考。