iOS UI 布局简史

date
Aug 13, 2019
slug
qlowiiyx
status
Published
tags
iOS
summary
type
Post
 
高能提醒:本文内容是个大杂烩,摘抄引用请见文章末尾的参考资料。另外,加了一些自己的见解!
日常开发中,UI 搭建、调试会占用我们大部分的时间,以至于移动端开发经常会被调侃为搭界面的。提高 UI 布局技术可以提高开发效率,把更多的时间放在优化、逻辑方面,而不是被界面业务绑死。

UI 布局技术概览

  • nib:nib 是 NeXT interface builder 的英文缩写,以二进制的形式存储界面信息,是 IB3.0 以前的文件格式。
  • xib:xib 是 xml interface builder 的英文缩写,是 IB3.0 之后苹果公司推出的新一代,以 xml 格式存储界面信息,在最终执行前,xib 文件会被编译为 nib 文件。。。。。。(请注意大神@史前图 的评论)
  • storyboard:故事版文件,是苹果最新推出的用于在界面开发中替代 xib 文件的一种新技术。本质上是一个 xml 文件的集中管理区,不但可以描述 xib 单个界面的结构,还可以描述界面之间的跳转及依赖关系。主要是靠手拖,感觉像积木玩具。
  • frame:等效于代码版的 storyboard,但更灵活。目前比较常用。如果没有好的适配方案,是多众多机型尺寸还是有点棘手。(欢迎补充最优雅的适配方案教程地址)
  • AutoLayout:自动布局(AutoLayout)是 iOS6 发布的界面布局技术,该算法的主要思想是:将基于约束系统的布局规则(本质上是表示视图布局关系的线性方程组)转化为表示规则的视图几何参数。实际上 AutoLayout 算法本身并非有 🍎 发明,只是苹果用 Objective-C 去实现了该算法,方便 iOS 开发者使用。AutoLayout 有多种使用方式,如 ① 可视化工具:Xcode 的 Interface Builder,② 纯代码:以Masonry为代表。更多内容见自动布局 Auto Layout (原理篇)
  • FlexBox:弹性布局(Flexible Box)。对,就是目前 Web 端最流行的布局方式(以前是盒子模型),现在 APP 上也能使用。此方案就扩展出很多技术,如Yoga(最牛逼的代表,Facebook 出品,衍生出很多上层方案,如跨平台的ReactNative、Android 的Litho、iOS 的Yogakit)、FlexboxLayout(Android 代表,Google 出品)、FlexLibFLEX等。(欢迎补充)

nib

xib

storyboard

frame

通过 frame 编写界面,主要难点在于机型适配,而且还比较繁琐。下面是适配的 2 个要点:

非刘海机型

notion image
16be1897f802a652
很明显能看出这三种屏幕的尺寸宽高比是差不多的,加上现在都屏幕尺寸都是 4.7+,因此常以 iPhone 6(s)为基准,进行等比缩放。在实际中可能整个页面全部、或部分节点、或仅缩放宽(或高,然后另一侧自适应) 。
// 在AppDelegate.h中
@property float autoSizeScaleX;
@property float autoSizeScaleY;

// 在AppDelegate.m中
#define ScreenHeight [[UIScreen mainScreen] bounds].size.height
#define ScreenWidth [[UIScreen mainScreen] bounds].size.width

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    AppDelegate *myDelegate = [[UIApplication sharedApplication] delegate];

    if(ScreenHeight > 667){ // 这里以(iPhone 6)为准
        myDelegate.autoSizeScaleX = ScreenWidth/375;
        myDelegate.autoSizeScaleY = ScreenHeight/667;
    } else {
        myDelegate.autoSizeScaleX = 1.0;
        myDelegate.autoSizeScaleY = 1.0;
    }
}
iPhone 6 屏幕的高度是 667,因此当屏幕尺寸大于 iPhone 6 时,autoSizeScaleX 和 autoSizeScaleY 即为当前屏幕和 iPhone 6 尺寸的宽高比。如果手机为 Iphone6 那么屏幕比例为 1,如果为 Iphone6s,屏幕比放大,Iphone5 就屏幕比缩小。现在我们获取了比例关系后,先来看一下如何解决代码设置界面时的适配。
CGRectMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height)这个方法使我们常用的设置尺寸的方法,在.m 文件中
CG_INLINE CGRect TS_CGRectMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
{
    AppDelegate *myDelegate = [[UIApplication sharedApplication] delegate];
    CGRect rect;
    rect.origin.x = x * myDelegate.autoSizeScaleX;
    rect.origin.y = y * myDelegate.autoSizeScaleY;
    rect.size.width = width * myDelegate.autoSizeScaleX;
    rect.size.height = height * myDelegate.autoSizeScaleY;
    return rect;
}
当我们使用的时候直接这样做UIImageView *imageview = [[UIImageView alloc] initWithFrame:TS_CGRectMake(100, 100, 50, 50)]; 这样我们得出的就是转换后的坐标了。这个 imageview 在 5、6、6 Plus 的位置和尺寸比例都是一样的。
不止是尺寸的适配,还有文字大小的适配。
#define MainScreenWidth [[UIScreen mainScreen] bounds].size.width

#define font(R) (R)*(MainScreenWidth)/375.0
所以就会经常看到下面的代码:
self.btnForgetPassWord = [UIButton alloc]initWithFrame:TS_CGRectMake(161, 499, 54, 12);
[self.btnForgetPassWord setFont:[UIFont systemFontOfSize:font(12)]];
总之,妈妈再也不用担心屏幕的适配了。

刘海机型

刘海比非刘海的区别在于多了一个安全区域 SafeArea,所以最简单的办法是移除非安全区域的尺寸,然后按照非刘海机型进行适配。
常用的方法方案就是宏!
#define K_iPhoneXStyle ( (CGSizeEqualToSize(CGSizeMake(414, 896), [[UIScreen mainScreen] bounds].size)) || ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO) )
// 或
#define K_iPhoneXStyle ((KScreenWidth == 375.f && KScreenHeight == 812.f ? YES : NO) || (KScreenWidth == 414.f && KScreenHeight == 896.f ? YES : NO))


// 其他宏
#define KScreenWidth ([UIScreen mainScreen].bounds.size.width)
#define KScreenHeight ([UIScreen mainScreen].bounds.size.height)
#define K_iPhoneXStyle ((KScreenWidth == 375.f && KScreenHeight == 812.f ? YES : NO) || (KScreenWidth == 414.f && KScreenHeight == 896.f ? YES : NO))
#define KStatusBarAndNavigationBarHeight (K_iPhoneXStyle ? 88.f : 64.f)
#define KStatusBarHeight (K_iPhoneXStyle ? 44.f : 20.f)
#define KTabbarHeight (K_iPhoneXStyle ? 83.f : 49.f)
#define KMagrinBottom (K_iPhoneXStyle ? 34.f : 0.f)

#define KScaleWidth(width) ((width)*(KScreenWidth/375.f))
#define IsIphone6P          SCREEN_WIDTH==414
#define SizeScale           (IsIphone6P ? 1.5 : 1)
#define kFontSize(value)    value*SizeScale
#define kFont(value)        [UIFont systemFontOfSize:value*SizeScale]
只考虑大部分情况其他哈~~例如横屏、新分辨率等就另说。

AutoLayout

和 frame 比起来灵活性有一定增强,将尺寸影响从整体降为局部,但是在更新时使用起来还是有一定麻烦,而且新引入优先级的概念。关于更新的几个方法的区别:
  • setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用 layoutSubviews。
  • layoutIfNeeded:告知页面布局立刻更新。所以一般都会和 setNeedsLayout 一起使用。如果希望立刻生成新的 frame 需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。
  • layoutSubviews:系统重写布局。
  • setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始。
  • updateConstraintsIfNeeded:告知立刻更新约束。
  • updateConstraints:系统更新约束。
更多关于 Masonry 的使用请移步Masonry 使用注意篇
AutoLayout 最大的问题在于需要精确的知道每一个 UI 元素的约束关系,当页面约束很复杂的时候,对程序员的细心和耐心是极大的考验。然而这根本不算是最严重的问题,一般页面都由若干个组件拼装而成,AutoLayout 最可怕的是不支持热插拔组件,即不可以销毁一个有其他组件依赖该组件约束的组件,程序员如果想要“删除”一个组件,需要添加一个引用指向该组件的宽或高,当需要“删除”时,将该变量设为 0,你以为就这么简单的完了?如果再将该组件“插入”回原处呢?
刚好 FlexBox 完美的解决了这个问题,请继续往后看。
我从 15 年开始转 iOS,当时跳过 xib、storyboard,直接学习的用 Masonry 纯代码式布局,所以我到现在也不会 Interface Builder。Masonry 给我的感觉是比较枯燥的,没有所见即所得的体验,而且一个 VC 或 View 里 UI 代码占用了很大部分。但是熬过初级后,和其他同事协同开发、灵活性、可控性、抽象复用等方便的体验是非常棒的!

FlexBox

终于轮到今天的主角了!开心~~有一点 Web 基础的可以去Flexbox 布局详解学习和练习。
Flexbox 解决了什么?
方向性 (传统布局方向是从左到右,从上至下)弹性伸缩 (传统尺寸定义是通过像素等来精确定义)元素对齐(可以做到插拔)
从上述几点看来,它似乎完美的解决了 iOS 原生布局开发效率低的问题,但它会增加页面的嵌套层级关系,在硬件性能饱和的情况下用空间换取开发效率。
我相信你对它的效率有疑虑!?(大神@史前图 的评论里指出 AutoLayout 的性能已经更新,所以请执行搜索相关最新的资料 😄)
这里根据从 AutoLayout 的布局算法谈性能里的测试代码进行修改,对 Frame/AutoLayout/FlexBox 进行布局,分段测算 10 ~ 350 个 UIView 的布局时间,取 100 次布局时间的平均值作为结果,耗时单位为秒。结果如下图:(测试布局的项目代码GitHub 地址
notion image
16be1ca4ac02ead4
虽然测试结果难免有偏差,但是根据折线图可以明显发现,FlexBox 的布局性能是比较接近 Frame 的。60FPS 作为一个 iOS 流畅度的黄金标准,要求布局在 0.0166667s 内完成,AutoLayout 在超过 50 个视图的时候,可能保持流畅就会开始有问题了。本次测试相关配置:Xcode9.2,iPad Pro (12.9-inch)(2nd generation) 模拟器。

Yoga

Yoga 最初源自 Facebook 在 2014 年的一个开源的 CSS 布局开源库,在 2016 年经过修改,更名为 Yoga。它是由 C 语言实现,基于 Flexbox 的一个编写视图的跨平台代码,让布局变得更简单,支持多个平台,包括 Java、C#、C、Objective-C 和 Swift。
Yoga Layout 官网。库开发者可以集成 Yoga 到他们的布局系统,就如 Facebook 已经集成进了它的两个开源项目:React NativeLithoComponentkit。另外 iOS 开发者可以直接用 YogaKit 来布局视图的框架。

React Native

RN 就不多说,目前市场上很多 APP 都在使用,应该说是 APP 跨平台开发的不二选择:
  • 携程:携程应该算是 APP 使用 RN 最多的厂家,当初还在 0.48 左右时就深度定制 RN,如热更新、分包等,网上能搜到很多携程 RN 的文章,如携程机票 React Native 整洁架构实践。终于在 2019 年开源了CRN,个人觉得开源得太晚了,对 RN 市场没有提供太大作用。
  • 华医通医生端:我的第一个 RN 产品,除了 IM 及相关的,其他页面均是 RN,占比估计 85%+。

Litho

Litho is a declarative framework for building efficient user interfaces (UI) on Android. It allows you to write highly-optimized Android views through a simple functional API based on Java annotations. It was primarily built to implement complex scrollable UIs based on RecyclerView. With Litho, you build your UI in terms of components instead of interacting directly with traditional Android views. A component is essentially a function that takes immutable inputs, called props, and returns a component hierarchy describing your user interface.
Litho 是高效构建 Android UI 的声明式框架,通过注解 API 创建高优的 Android 视图,非常适用于基于 Recyclerview 的复杂滚动列表。Litho 使用一系列组件构建视图,代替了 Android 传统视图交互方式。组件本质上是一个函数,它接受名为 Props 的不可变输入,并返回描述用户界面的组件层次结构。
Litho 是一套完全不同于传统 Android 的 UI 框架,它继承了 Facebook 一向大胆创新的风格,突破性地在 Android 上实现了 React 风格的 UI 框架。架构图如下:
notion image
16c37bfae2740b5f
我对 Android 开发不熟,所以请移步基本功 | Litho 的使用及原理剖析

YogaKit

iOS 端 Yoga 的上层 UI 库 Componentkit,但是我看文档之后觉得非常难用,所以略过。
但是YogaKit还不错,同时支持 OC 和 Swift,Yoga 教程-使用跨平台布局引擎 这份基于 Swift 的 YogaKit 教程还不错。
ps:我本想找一个 Masonry 的替代者,特别希望能支持 Flex 布局,所以抱有很大希望去了解和学习 YogaKit,但是失望 😞 了。第一是代码繁琐,第二是 YogaKit 的使用者不多。我还是好好研究 RN 吧~

FlexboxLayout

FlexboxLayout是谷歌出的,只支持 Android,所以期待各位大神关于此框架的评论。
顺便吐槽一句,google 是看着什么语言和技术火,都会插一脚,然后独自开发一套。如 golang、angular、android studio、dart、flutter 等,加上国内技术开发者的 google 情节,所以你们自己体会。。。除了 JS 引擎——V8

swiftUI

iOS 上的 UI,我觉得 swiftUI 才是以后的王者!我不接受反驳,哈哈哈哈哈哈…iOS 的 UI 开发语言就 objective-c 和 swift,从编译型的 oc 变为解释型的 swift,从语言角度看跨度还是挺大的,在众多技术均在快速发展的现代,解释型语言更能适应未来,不过 swift 还未在跨平台上发挥出任何两点,就连在 iPhone 和 mac 上都没。
如果没有 swift 的从入门到再入门事件,可能 swift 的市场又是另外一番景象!真希望 swift 5 能稳定下来。

后语

UI 的基础布局也就那么回事,都是从图形绘制 OpenGL,到 UI Library,再到 UI Controls,再到 APP,电脑发展到现在一直没有变过,如 Windows 上的 MFC、iOS 上的 UIKit、刚出来的 flutter 等。
此处借鉴 TCP/IP 的七层模型(我觉得这是世界上最牛逼的架构,没有之一,不接受反驳!),只有分层之后,不同的人员才能扮演不同的角色,去完成对应的工作。我们说的 UI 是软件层面的,是运行在硬件上的,所以软件到硬件的衔接要靠图形绘制,这个工作是由操作系统去完成。
有了操作系统这个大环境,为了建立生态,必须对外提供响应的开发套件,如 UI 库、网络库、驱动库等,这一点在 Windows 提现特别明显。然后开发者才会加入你的开发阵营,然后你的市场才会逐步扩大,所以为啥经常听说一门语言和技术背后,也需要强大的公司和资金来支持,比如微软、谷歌、亚马逊、Facebook、众多第三方厂商的 SDK 等等,请注意像阿里云 SDK、腾讯 SDK 等各个厂商也算是这一层的,可以把 Windows、Android、iOS 等操作系统看着是一级厂商,把 Facebook、阿里云、腾讯云等服务提供商是二级厂商。
有了各个等级厂商提供的 Library 和对应的 Control,然后才能更好的创造出百花齐放的 APP,才有我们这种出现不同方向的软件开发者。所以看待一个新的 UI 库或技术,需要从其原理和应用分析,找到其合适的定位,如果你没有深入分析背后的技术,是无法发挥其最大价值的。
就像 React Native,你特么叫我去做 IM 和多媒体,老子一巴掌给你飞过来!RN 它只是写降低了 APP UI 的门槛,扩大了其受众者,就如 React 的口号『learn once write anywhere』。RN 的大致原理是通过 JavaScript 和 JavaScript 解释器,去动态控制原生的 UI 布局,和以前有大神通过解析 XML 去自动生成 UI,是一样的道理,又如Lua这个同 JavaScript 语法类似的脚本在游戏行业活得风生水起,RN 的热更新原理就是这么来的——通过更新 JavaScript 文件达到更新 UI 布局和业务逻辑等目的,所以 RN 的核心是在具有 ES 6/7/8/…的 JavaScript 和对应的 JavaScript 解释器,关于 RN 中的 JavaScript 解释器,可以看看逆袭 Futter? Facebook 发布全新跨平台引擎 Hermes!JS 引擎大 PK:JSC vs V8 vs Hermes
不限于 RN,你想要做得更好更优秀,你需要多研究。不要想着你会 React 网页,就觉得可以来做 APP,呵呵,一个库依赖问题就能把你搞死。不是你长的美,就可以想得美!就好比让你用 openGL 的 API 来画一个控件,你画不好,然后就说 openGL 技术不行?!这时我只能送你一个字,滚!
说了这么多,我不是希望任一一个 UI 库有多牛逼,而是希望 FlexBox 这个 UI 规范能在 APP 端能扩大影响,尽量和在布局上更优秀的 Web 端保持一致,最终实现统一,做到最根本的跨平台,但是可以有很多库或者方案来实现。

2019-07-28 更新: 下面评论 @小二 Flutter 提到的MyLinearLayout也很优秀很强大,新旧布局方案均兼容,各位技术官可以去尝试一下,说不定你也喜欢。但是我觉得文档不是完善,特别是使用方面的,会导致上手难度增大。
下面评论 @史前图腾 提到的 iOS 布局历史,本文中提到的有些不对,请注意甄别!(我想修改,但是不知道怎么修改 😔)。
参考资料:(排名不分先后)

© 刘德华 2020 - 2023