Objective-C 在 iOS 用户界面适配中的技巧
2023-06-095.7k 阅读
一、理解 iOS 设备屏幕尺寸与分辨率
(一)不同 iOS 设备的屏幕尺寸
iOS 生态中存在多种设备,从 iPhone 到 iPad,它们的屏幕尺寸各不相同。例如,iPhone 12 mini 的屏幕尺寸为 5.4 英寸,而 iPhone 12 Pro Max 的屏幕尺寸则达到 6.7 英寸。iPad 系列更是有多种尺寸,如 iPad Air 为 10.9 英寸,iPad Pro 有 11 英寸和 12.9 英寸等。这些不同的尺寸意味着在进行用户界面适配时,需要考虑元素的布局和大小如何在不同屏幕上合理呈现。
(二)分辨率与点的概念
- 分辨率:指屏幕水平和垂直方向上的像素数量。例如,iPhone 12 的分辨率为 2532×1170 像素。然而,直接使用像素来设计界面并不合适,因为不同设备像素密度不同。
- 点(point):是 iOS 开发中用于布局和绘图的抽象单位。在标准密度屏幕(1x)上,1 点等于 1 像素;在 Retina 屏幕(2x 或 3x)上,1 点对应 2×2 或 3×3 像素。例如,在 iPhone 8(2x 屏幕)上,一个 100×100 点的按钮,实际在屏幕上占据 200×200 像素。理解点与像素的关系对于正确适配界面至关重要。
二、使用 Auto Layout 进行界面适配
(一)Auto Layout 基础
- 约束(Constraints):Auto Layout 通过添加约束来定义视图之间的关系和大小。例如,我们可以设置一个按钮距离其父视图的左边距为 20 点,上边缘距离为 30 点,宽度为 100 点,高度为 40 点。这些约束可以在 Interface Builder 中直观地设置,也可以通过代码实现。
- 优先级(Priority):有些情况下,约束之间可能会产生冲突。这时可以为约束设置优先级,优先级高的约束会优先被满足。例如,在一个自适应布局中,我们希望某个视图在宽度变化时优先保持水平居中,但同时也有最小宽度的限制,就可以通过设置不同的优先级来解决潜在冲突。
(二)在 Objective - C 中使用 Auto Layout 代码示例
- 创建视图并添加约束
// 创建一个红色的视图
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
// 添加约束,使其距离父视图左边距 20 点,上边缘 20 点,宽度 100 点,高度 100 点
[redView.translatesAutoresizingMaskIntoConstraints setValue:@NO];
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:redView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:20];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:redView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:20];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:redView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:100];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:redView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:100];
[self.view addConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]];
- 动态更新约束 有时候,我们需要根据不同的条件动态更新视图的约束。例如,当设备旋转时,某个视图可能需要改变宽度或高度。
// 假设我们有一个视图,在竖屏时宽度为屏幕宽度的一半,横屏时宽度为屏幕宽度的三分之一
@property (nonatomic, strong) NSLayoutConstraint *widthConstraint;
// 在视图加载时设置初始约束
- (void)viewDidLoad {
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.backgroundColor = [UIColor blueColor];
[self.view addSubview:myView];
myView.translatesAutoresizingMaskIntoConstraints = NO;
// 初始竖屏约束
self.widthConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:0];
NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[self.view addConstraints:@[self.widthConstraint, centerXConstraint, centerYConstraint]];
}
// 监听设备旋转通知,更新约束
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) {
self.widthConstraint.multiplier = 0.5;
} else {
self.widthConstraint.multiplier = 1.0 / 3.0;
}
[self.view layoutIfNeeded];
}
三、Size Classes 与适应性布局
(一)Size Classes 概述
- Width Class 和 Height Class:Size Classes 是 iOS 8 引入的一种布局概念,用于根据设备的屏幕宽度和高度进行布局调整。Width Class 分为 Compact 和 Regular,Height Class 同样分为 Compact 和 Regular。例如,在 iPhone 竖屏时,Width Class 为 Compact,Height Class 为 Regular;而在 iPad 竖屏时,Width Class 和 Height Class 都为 Regular。
- Trait Collection:每个视图都有一个 traitCollection 属性,它包含了当前视图的 Size Classes 信息。通过检测 traitCollection,我们可以根据不同的 Size Classes 来加载不同的布局。
(二)在 Interface Builder 中使用 Size Classes
- 设置不同 Size Classes 的布局:在 Interface Builder 中,我们可以通过选择不同的 Size Classes 组合,为同一个视图控制器设置不同的布局。例如,在 iPhone 竖屏(Compact Width, Regular Height)时,某个按钮可能在屏幕底部居中;而在 iPad 竖屏(Regular Width, Regular Height)时,该按钮可能在左上角。
- 使用 Size - Class - Specific Constraints:我们可以为不同的 Size Classes 单独设置约束。在 Interface Builder 中,选中一个约束后,可以在 Attributes Inspector 中设置该约束适用的 Size Classes。这样,当设备的 Size Classes 发生变化时,相应的约束会生效,从而实现布局的自适应。
(三)在代码中检测 Size Classes
// 检测当前视图的 Size Classes
- (void)viewDidLoad {
[super viewDidLoad];
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
// 当前是 iPhone 竖屏布局
// 可以在这里动态加载适合该布局的视图或调整现有视图的属性
UIView *compactView = [[UIView alloc] init];
compactView.backgroundColor = [UIColor greenColor];
[self.view addSubview:compactView];
compactView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:compactView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:compactView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[self.view addConstraints:@[centerXConstraint, centerYConstraint]];
} else if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular &&
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
// 当前可能是 iPad 竖屏或横屏布局
// 进行相应的布局调整
UIView *regularView = [[UIView alloc] init];
regularView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:regularView];
regularView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:regularView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:20];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:regularView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:20];
[self.view addConstraints:@[leftConstraint, topConstraint]];
}
}
四、图像资源适配
(一)不同分辨率的图像资源
- @1x、@2x 和 @3x 图像:为了适配不同像素密度的屏幕,iOS 开发中需要提供不同分辨率的图像资源。@1x 图像适用于标准密度屏幕,@2x 图像适用于 Retina(2x)屏幕,@3x 图像适用于更高像素密度(3x)的屏幕,如 iPhone 12 Pro Max。在项目中,我们通常将这些图像命名为相同的文件名,只是后缀不同,例如
icon.png
、icon@2x.png
和icon@3x.png
。 - 加载合适的图像:在 Objective - C 中,UIImage 类会根据设备的屏幕分辨率自动加载合适的图像资源。例如:
UIImage *image = [UIImage imageNamed:@"icon"];
// 如果在 2x 屏幕设备上运行,UIImage 会自动加载 icon@2x.png
// 如果在 3x 屏幕设备上运行,UIImage 会自动加载 icon@3x.png
(二)自适应图像
- 矢量图像(PDF):对于一些需要在不同尺寸和分辨率下保持清晰的图标或图形,可以使用矢量图像,如 PDF 格式。在 iOS 中,可以将 PDF 图像添加到项目中,并使用
UIImage
的imageWithPDFNamed:
方法(需要自定义扩展方法实现)来加载。矢量图像在任何分辨率下都能完美显示,不会出现模糊问题。 - 可拉伸图像:有些图像,如按钮背景,需要根据内容大小自动拉伸。可以使用
UIImage
的resizableImageWithCapInsets:
方法来创建可拉伸图像。例如,对于一个带有圆角的按钮背景图像:
UIImage *buttonBackground = [UIImage imageNamed:@"button_bg"];
// 设置拉伸区域,这里假设按钮背景图像的上下左右各 10 像素为不拉伸区域
UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 10, 10);
UIImage *resizableBackground = [buttonBackground resizableImageWithCapInsets:insets];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:resizableBackground forState:UIControlStateNormal];
五、字体适配
(一)系统字体与动态类型
- 系统字体:iOS 提供了多种系统字体,使用系统字体可以确保在不同设备上的一致性和良好的可读性。可以通过
[UIFont systemFontOfSize:size]
方法获取系统字体,其中size
为字体大小。例如:
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:16];
label.text = @"Hello, iOS";
[self.view addSubview:label];
- 动态类型:iOS 支持动态字体,用户可以在系统设置中调整字体大小。为了适配动态字体,我们可以使用
preferredFontForTextStyle:
方法。例如,将一个标签设置为标题样式,并适配动态字体:
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
titleLabel.text = @"Dynamic Type Title";
[self.view addSubview:titleLabel];
(二)自定义字体适配
- 添加自定义字体:如果项目中需要使用自定义字体,首先要将字体文件添加到项目中,并在
Info.plist
文件中声明字体名称。例如,将MyCustomFont.ttf
添加到项目后,在Info.plist
中添加UIAppFonts
数组,并将字体文件名作为数组元素。 - 使用自定义字体:在代码中使用自定义字体时,通过
[UIFont fontWithName:name size:size]
方法,其中name
为字体名称,size
为字体大小。例如:
UIFont *customFont = [UIFont fontWithName:@"MyCustomFont" size:16];
UILabel *customLabel = [[UILabel alloc] init];
customLabel.font = customFont;
customLabel.text = @"Custom Font Text";
[self.view addSubview:customLabel];
然而,在使用自定义字体时,也需要考虑不同屏幕尺寸和动态类型设置下的可读性和布局问题。可以结合动态类型的概念,根据系统设置的字体大小动态调整自定义字体的显示大小,以保证在各种情况下都有良好的用户体验。
六、处理设备旋转
(一)监听设备旋转通知
- 注册通知:可以通过注册
UIDeviceOrientationDidChangeNotification
通知来监听设备旋转事件。例如:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceOrientationDidChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
- (void)deviceOrientationDidChange:(NSNotification *)notification {
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
if (orientation == UIDeviceOrientationLandscapeLeft ||
orientation == UIDeviceOrientationLandscapeRight) {
// 横屏处理逻辑
// 例如,调整视图的布局约束
// 假设我们有一个视图,在横屏时需要改变宽度
self.someViewWidthConstraint.constant = 200;
[self.view layoutIfNeeded];
} else if (orientation == UIDeviceOrientationPortrait ||
orientation == UIDeviceOrientationPortraitUpsideDown) {
// 竖屏处理逻辑
self.someViewWidthConstraint.constant = 100;
[self.view layoutIfNeeded];
}
}
- 移除通知:为了避免内存泄漏,在视图控制器销毁时,需要移除通知观察者。
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
(二)使用 Auto Layout 处理旋转
- 约束的自动更新:使用 Auto Layout 时,许多布局约束会在设备旋转时自动更新,以适应新的屏幕方向。例如,一个居中显示的按钮,在设备旋转后,仍然会保持在新屏幕的居中位置,因为其水平和垂直居中的约束会自动根据新的屏幕尺寸调整。
- 方向特定的约束:有时候,我们需要在不同方向上使用不同的约束。可以通过创建两组不同的约束,并在设备旋转时激活或禁用相应的约束组。例如:
// 假设我们有一个视图,在竖屏和横屏时有不同的宽度约束
@property (nonatomic, strong) NSLayoutConstraint *portraitWidthConstraint;
@property (nonatomic, strong) NSLayoutConstraint *landscapeWidthConstraint;
- (void)viewDidLoad {
[super viewDidLoad];
UIView *myView = [[UIView alloc] init];
myView.backgroundColor = [UIColor purpleColor];
[self.view addSubview:myView];
myView.translatesAutoresizingMaskIntoConstraints = NO;
self.portraitWidthConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.8
constant:0];
self.landscapeWidthConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:0];
NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[self.view addConstraints:@[self.portraitWidthConstraint, centerXConstraint, centerYConstraint]];
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) {
[self.view removeConstraint:self.landscapeWidthConstraint];
[self.view addConstraint:self.portraitWidthConstraint];
} else {
[self.view removeConstraint:self.portraitWidthConstraint];
[self.view addConstraint:self.landscapeWidthConstraint];
}
[self.view layoutIfNeeded];
}
七、iPad 与 iPhone 布局差异处理
(一)iPad 特定布局
- 分屏多任务:iPad 支持分屏多任务功能,这意味着应用程序可能需要在不同的窗口大小下进行布局调整。可以通过检测窗口的大小和 Size Classes 来实现分屏时的适配。例如,在分屏时,某个视图可能需要缩小或隐藏部分内容以适应较小的空间。
- popover 视图:iPad 经常使用 popover 视图来显示额外的信息或操作选项。在 Objective - C 中,可以使用
UIPopoverPresentationController
来创建和管理 popover 视图。例如:
UIViewController *popoverViewController = [[UIViewController alloc] init];
popoverViewController.view.backgroundColor = [UIColor whiteColor];
popoverViewController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popoverController = popoverViewController.popoverPresentationController;
popoverController.sourceView = self.someButton;
popoverController.sourceRect = self.someButton.bounds;
popoverController.permittedArrowDirections = UIPopoverArrowDirectionAny;
[self presentViewController:popoverViewController animated:YES completion:nil];
(二)通用应用的布局策略
- 使用 Size Classes 区分:通过 Size Classes 可以有效地为 iPhone 和 iPad 设计不同的布局。在通用应用项目中,利用 Interface Builder 中的 Size Classes 功能,为不同设备设置合适的视图布局和约束。例如,在 iPhone 上,某个导航栏可能采用底部 tab 栏的形式,而在 iPad 上,可能采用侧边栏的形式。
- 代码逻辑判断:在代码中,可以通过检测设备类型和 Size Classes 来执行不同的逻辑。例如:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// iPad 相关逻辑
// 例如,加载 iPad 特定的视图或调整布局
UIView *ipadView = [[UIView alloc] init];
ipadView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:ipadView];
ipadView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:ipadView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:20];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:ipadView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:20];
[self.view addConstraints:@[leftConstraint, topConstraint]];
} else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
// iPhone 相关逻辑
UIView *iphoneView = [[UIView alloc] init];
iphoneView.backgroundColor = [UIColor cyanColor];
[self.view addSubview:iphoneView];
iphoneView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *centerXConstraint = [NSLayoutConstraint constraintWithItem:iphoneView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0];
NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:iphoneView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0];
[self.view addConstraints:@[centerXConstraint, centerYConstraint]];
}
通过以上多种技巧和方法的综合运用,在 Objective - C 开发 iOS 应用时,能够有效地实现用户界面在不同 iOS 设备上的适配,提供一致且良好的用户体验。无论是使用 Auto Layout 进行布局管理,还是根据 Size Classes 调整布局,以及处理图像、字体、设备旋转和不同设备类型的差异,每一个方面都相互关联,共同构成了一个完整的适配体系。开发者需要深入理解这些概念,并在实际项目中灵活运用,以应对 iOS 生态中多样化的设备和屏幕尺寸。