区别 iPhone 做布局

现在主流有三种不同的 iPhone 尺寸:4 英寸、4.7 英寸以及 5.5 英寸。从设计上来讲,为特定的屏幕分别优化是很有必要的,当然,这就需要开发者做一些额外的工作。

作者:@nixzhu


虽然有所谓的 Adaptive Layout,但依其对屏幕的区分方式,我们并不能区分 iPhone 的不同屏幕尺寸,至少在应用竖屏时不行。

所以,我们依然需要用代码检测不同的屏幕尺寸,然后以其为基准来为界面元素设定如边距、大小之类的参数,以实现不同屏幕下最优的显示效果。

大家一定都写过(或使用过)判断 iPhone 型号的代码,并无甚特别。但重要的是在使用的层面,怎样设计优雅的 API 来完成不同屏幕的适配呢?

首先,我们设计屏幕的尺寸模型,因为和 UIDevice 有关,我们就扩展 UIDevice,有 enum 如下:

  1. import UIKit
  2. extension UIDevice {
  3. enum ScreenModel {
  4. case Classic
  5. case Bigger
  6. case BiggerPlus
  7. }
  8. // TODO
  9. }

注意,这里只从竖屏宽度上区分,将 3.5 英寸和 4 英寸都当作 Classic,如果你的应用还要区分 3.5 英寸的设备,那要对应增加 case。

然后,我们实现一个屏幕模型的单例,很明显它只需要初始计算一次即可,因此:

  1. static let screenModel: ScreenModel = {
  2. let screen = UIScreen.mainScreen()
  3. let nativeWidth = screen.nativeBounds.size.width
  4. if nativeWidth == 320 * 2 {
  5. return .Classic
  6. } else if nativeWidth == 375 * 2 {
  7. return .Bigger
  8. } else if nativeWidth == 414 * 3 {
  9. return .BiggerPlus
  10. }
  11. return .Bigger // Default
  12. }()
  13. // TODO

我们利用 iOS 8 中 UIScreen 的 nativeBounds 来做判断。据其文档描述:

The bounding rectangle of the physical screen, measured in pixels. (read-only)

This rectangle is based on the device in a portrait-up orientation. This value does not change as the device rotates.

它表示设备在竖屏时的物理分辨率。有了它,我们就不需要关心两个 Bigger iPhone 的放大模式了。当然,若你的应用需要兼容 iOS 7 或以下版本,那就要换成其他的判断方法。如前所述,这并不是重点。

最后,我们设计一个 API 以对不同的屏幕设置不同的参数。我们需要它使用起来简单,那我们就设定其为 UIDevice 的类方法:

  1. class func matchMarginFrom(classic: CGFloat, _ bigger: CGFloat, _ biggerPlus: CGFloat) -> CGFloat {
  2. switch screenModel {
  3. case .Classic:
  4. return classic
  5. case .Bigger:
  6. return bigger
  7. case .BiggerPlus:
  8. return biggerPlus
  9. }
  10. }

注意其参数前的 _,这表示在使用时我们不需要写参数名,因此如果我要设定某个元素距离左边的距离,那使用起来的感觉如下:

  1. let leftEdge = UIDevice.matchMarginFrom(15, 30, 40)

之后无论你要用其设置 AutoLayout 约束还是计算 CGRect 都可以。

如果没有这个 API 的话,那在每一个需要区别屏幕的地方我们都需要写一个 Switch 语句根据 screenModel 来判断。

另外,如果将来 iPhone 又出了新的型号以至于我们要增加新的屏幕模型,那只需要修改对应增加 ScreenModel 的 case,再进一步修改 matchMarginFrom 的实现。之后所有使用 matchMarginFrom 的地方都会编译失败,这正好给了我们补足新数据的机会,而不用担心匆忙中漏掉某一个。

而如果使用可变参数或者用传递数组的方式来实现 matchMarginFrom 就得不到编译器帮我们检查的好处。如果真有增加 ScreenModel case 的一天,那改起所有使用 matchMarginFrom 的地方就不保险了。

因为代码比较简单,就放在 gist https://gist.github.com/nixzhu/3c8ed0b8f7f24df924ac 里了。

7月22日补记:已有更合理的 Repo:https://github.com/nixzhu/Ruler


欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog