MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

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 英寸等。这些不同的尺寸意味着在进行用户界面适配时,需要考虑元素的布局和大小如何在不同屏幕上合理呈现。

(二)分辨率与点的概念

  1. 分辨率:指屏幕水平和垂直方向上的像素数量。例如,iPhone 12 的分辨率为 2532×1170 像素。然而,直接使用像素来设计界面并不合适,因为不同设备像素密度不同。
  2. 点(point):是 iOS 开发中用于布局和绘图的抽象单位。在标准密度屏幕(1x)上,1 点等于 1 像素;在 Retina 屏幕(2x 或 3x)上,1 点对应 2×2 或 3×3 像素。例如,在 iPhone 8(2x 屏幕)上,一个 100×100 点的按钮,实际在屏幕上占据 200×200 像素。理解点与像素的关系对于正确适配界面至关重要。

二、使用 Auto Layout 进行界面适配

(一)Auto Layout 基础

  1. 约束(Constraints):Auto Layout 通过添加约束来定义视图之间的关系和大小。例如,我们可以设置一个按钮距离其父视图的左边距为 20 点,上边缘距离为 30 点,宽度为 100 点,高度为 40 点。这些约束可以在 Interface Builder 中直观地设置,也可以通过代码实现。
  2. 优先级(Priority):有些情况下,约束之间可能会产生冲突。这时可以为约束设置优先级,优先级高的约束会优先被满足。例如,在一个自适应布局中,我们希望某个视图在宽度变化时优先保持水平居中,但同时也有最小宽度的限制,就可以通过设置不同的优先级来解决潜在冲突。

(二)在 Objective - C 中使用 Auto Layout 代码示例

  1. 创建视图并添加约束
// 创建一个红色的视图
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]];
  1. 动态更新约束 有时候,我们需要根据不同的条件动态更新视图的约束。例如,当设备旋转时,某个视图可能需要改变宽度或高度。
// 假设我们有一个视图,在竖屏时宽度为屏幕宽度的一半,横屏时宽度为屏幕宽度的三分之一
@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 概述

  1. 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。
  2. Trait Collection:每个视图都有一个 traitCollection 属性,它包含了当前视图的 Size Classes 信息。通过检测 traitCollection,我们可以根据不同的 Size Classes 来加载不同的布局。

(二)在 Interface Builder 中使用 Size Classes

  1. 设置不同 Size Classes 的布局:在 Interface Builder 中,我们可以通过选择不同的 Size Classes 组合,为同一个视图控制器设置不同的布局。例如,在 iPhone 竖屏(Compact Width, Regular Height)时,某个按钮可能在屏幕底部居中;而在 iPad 竖屏(Regular Width, Regular Height)时,该按钮可能在左上角。
  2. 使用 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]];
    }
}

四、图像资源适配

(一)不同分辨率的图像资源

  1. @1x、@2x 和 @3x 图像:为了适配不同像素密度的屏幕,iOS 开发中需要提供不同分辨率的图像资源。@1x 图像适用于标准密度屏幕,@2x 图像适用于 Retina(2x)屏幕,@3x 图像适用于更高像素密度(3x)的屏幕,如 iPhone 12 Pro Max。在项目中,我们通常将这些图像命名为相同的文件名,只是后缀不同,例如 icon.pngicon@2x.pngicon@3x.png
  2. 加载合适的图像:在 Objective - C 中,UIImage 类会根据设备的屏幕分辨率自动加载合适的图像资源。例如:
UIImage *image = [UIImage imageNamed:@"icon"];
// 如果在 2x 屏幕设备上运行,UIImage 会自动加载 icon@2x.png
// 如果在 3x 屏幕设备上运行,UIImage 会自动加载 icon@3x.png

(二)自适应图像

  1. 矢量图像(PDF):对于一些需要在不同尺寸和分辨率下保持清晰的图标或图形,可以使用矢量图像,如 PDF 格式。在 iOS 中,可以将 PDF 图像添加到项目中,并使用 UIImageimageWithPDFNamed: 方法(需要自定义扩展方法实现)来加载。矢量图像在任何分辨率下都能完美显示,不会出现模糊问题。
  2. 可拉伸图像:有些图像,如按钮背景,需要根据内容大小自动拉伸。可以使用 UIImageresizableImageWithCapInsets: 方法来创建可拉伸图像。例如,对于一个带有圆角的按钮背景图像:
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];

五、字体适配

(一)系统字体与动态类型

  1. 系统字体:iOS 提供了多种系统字体,使用系统字体可以确保在不同设备上的一致性和良好的可读性。可以通过 [UIFont systemFontOfSize:size] 方法获取系统字体,其中 size 为字体大小。例如:
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:16];
label.text = @"Hello, iOS";
[self.view addSubview:label];
  1. 动态类型:iOS 支持动态字体,用户可以在系统设置中调整字体大小。为了适配动态字体,我们可以使用 preferredFontForTextStyle: 方法。例如,将一个标签设置为标题样式,并适配动态字体:
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
titleLabel.text = @"Dynamic Type Title";
[self.view addSubview:titleLabel];

(二)自定义字体适配

  1. 添加自定义字体:如果项目中需要使用自定义字体,首先要将字体文件添加到项目中,并在 Info.plist 文件中声明字体名称。例如,将 MyCustomFont.ttf 添加到项目后,在 Info.plist 中添加 UIAppFonts 数组,并将字体文件名作为数组元素。
  2. 使用自定义字体:在代码中使用自定义字体时,通过 [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];

然而,在使用自定义字体时,也需要考虑不同屏幕尺寸和动态类型设置下的可读性和布局问题。可以结合动态类型的概念,根据系统设置的字体大小动态调整自定义字体的显示大小,以保证在各种情况下都有良好的用户体验。

六、处理设备旋转

(一)监听设备旋转通知

  1. 注册通知:可以通过注册 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];
    }
}
  1. 移除通知:为了避免内存泄漏,在视图控制器销毁时,需要移除通知观察者。
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIDeviceOrientationDidChangeNotification
                                                  object:nil];
}

(二)使用 Auto Layout 处理旋转

  1. 约束的自动更新:使用 Auto Layout 时,许多布局约束会在设备旋转时自动更新,以适应新的屏幕方向。例如,一个居中显示的按钮,在设备旋转后,仍然会保持在新屏幕的居中位置,因为其水平和垂直居中的约束会自动根据新的屏幕尺寸调整。
  2. 方向特定的约束:有时候,我们需要在不同方向上使用不同的约束。可以通过创建两组不同的约束,并在设备旋转时激活或禁用相应的约束组。例如:
// 假设我们有一个视图,在竖屏和横屏时有不同的宽度约束
@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 特定布局

  1. 分屏多任务:iPad 支持分屏多任务功能,这意味着应用程序可能需要在不同的窗口大小下进行布局调整。可以通过检测窗口的大小和 Size Classes 来实现分屏时的适配。例如,在分屏时,某个视图可能需要缩小或隐藏部分内容以适应较小的空间。
  2. 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];

(二)通用应用的布局策略

  1. 使用 Size Classes 区分:通过 Size Classes 可以有效地为 iPhone 和 iPad 设计不同的布局。在通用应用项目中,利用 Interface Builder 中的 Size Classes 功能,为不同设备设置合适的视图布局和约束。例如,在 iPhone 上,某个导航栏可能采用底部 tab 栏的形式,而在 iPad 上,可能采用侧边栏的形式。
  2. 代码逻辑判断:在代码中,可以通过检测设备类型和 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 生态中多样化的设备和屏幕尺寸。