Skip to content

Instantly share code, notes, and snippets.

@Josscii Josscii/ios layout.md
Last active Jun 7, 2018

Embed
What would you like to do?
iOS 布局总结

iOS 布局总结

写了这么久的 iOS,基本都是和界面布局打交道,平常在编码的过程中也逐渐积累了一些关于布局的心得,这里做个总结,既是对前面工作的总结,也希望能够给读这篇文章的人一些收获。

ViewController 的 View

从 iOS 7 开始到 iOS 10,由于有了高斯模糊的 bar,ViewController 默认开启全屏布局,UIViewController 增加了两个属性:

open var edgesForExtendedLayout: UIRectEdge // Defaults to UIRectEdgeAll
open var extendedLayoutIncludesOpaqueBars: Bool // Defaults to NO, but bars are translucent by default on 7_0.

edgesForExtendedLayout 定义了 view 的哪些边缘可以延伸到 bar 的下方,默认为 all。当其值为空时,view 会改变自己的 frame 来使子视图不被遮挡,比如有 navigationBar,那么 view 的 frame 就会变成 (0, 64, 320, 603)。

extendedLayoutIncludesOpaqueBars 来决定当 bar 为不透明时,也就是 isTranslucent 为 false 时,view 是否也会做出相应的调整。

在 iOS 11 之前,scrollView 作为 UIViewController 的 view 最常见的子视图,为了适应这一变化,UIViewController 增加了一个属性:

open var automaticallyAdjustsScrollViewInsets: Bool // Defaults to YES

当 scrollView 为 UIViewController 的 view 的第一个子视图且这个属性为 true 时,系统会自动调整 scrollView 的 contentInsets 来保证 ScrollView 的 content 不被 bar 遮挡住,比如有 navigationBar 和 tabBar,那么 scrollView 的 contentInsets 就为 (64, 0, 49, 0)。

注意:这个属性并不会对仅有 statusBar 的情况做出响应。

另外,对于 childViewController,这个属性会失效,解决办法看这里

对于其他的子视图,UIViewController 增加了另外两个属性:

open var topLayoutGuide: UILayoutSupport { get }
open var bottomLayoutGuide: UILayoutSupport { get }

这两个属性是配合 autolayout 使用的,它们的 length 随着 bar (包括 statusBar)的有无而变化,只要相对于这两个属性布局,那么就能保证子视图不被 bar 遮挡。

当然,如果使用 frame 布局的话,只能由开发者自己通过加减 magic number 去适配了。

从 iOS 11 开始,针对 iPhone X 的屏幕,苹果提出了新的解决方案 — safe area,这次他们转变了思路,把原来由 ViewController 控制改为由 View 控制。

UIView 新增了两个属性:

@available(iOS 11.0, *)
open var safeAreaLayoutGuide: UILayoutGuide { get }

@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get }

前一个用于子视图 autolayout 的相对布局,后一个用于 frame 的相对布局。

思路是这样的,对于 vc 的 root view 来说,它的 safe area 是除去 bar 以外的区域,而对于 root view 的第一级子视图(假定为 v)来说,它的 safe area 是自己和 root view 的 safe area 相交的部分。

注意:safe area 不会传递到 v 的子视图。

所以 safe area 其实可以翻译为子视图布局的安全区域

UIScrollView 新增了两个属性:

@available(iOS 11.0, *)
open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior

@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets { get }

因此,scrollView 作为 view 的第一个子视图时,行为变得更智能了。

scrollView 的 contentInset 会随着 scrollView 的 frame 的变化而变化,也就说,scrollView 被遮挡多少,contentInset 就会补回来。尽管经过我测试,这个值的最大范围为 UIEdgeInsets(top: 88.0, left: 0.0, bottom: 83.0, right: 0.0)。

值得注意的要取 adjustedContentInset 的值才是调整的过的 contentInset 的值。

并且因为 iPhone X 的刘海,相较于以前的改进是,现在调整 contentInsets 会把 statusBar 考虑在内了。

最后一个问题是关于 xib 的,如果要支持 iOS 11 以下,在 xib 里面,topLayoutGuide 可以当做 safeAreaInsets 来用,效果是一样的。

参考链接:

iOS7 Day-by-Day :: Day 20 :: View controller content and navigation bars

frame 布局

1,添加 extension

用 frame 布局一大头疼的问题的是,每次存取 view 的跟 frame 相关的值的时候都要写老大一串,所以最好的解决方案就是提前写好分类,像这样:

extension UIView {
    var left: CGFloat {
        set {
            frame.origin.x = newValue
        }
        
        get {
            return frame.origin.x
        }
    }
    ...
}

2,子视图布局时机

由于在布局过程中相对父视图或者其他子视图布局是不可避免的,而多行文本 label 或者其他动态视图的 size 又不确定,我们常常只有延迟我们的布局计算到 layoutSubviews 中。layoutSubviews 可以说是在 view 显示之前你的最后一次布局的地方了。对于 frame 布局来说,UIView 的这个方法理论上来说什么都不会做,但是我们还是必须要调用 super 的方法。并且值得注意的是,我们一般在这个方法里只做最后的布局调整,而不应该在里面频繁的创建添加子视图。

3,sizeToFit 和 sizeThatFits

sizeThatFits 的默认实现返回的是当前的 frame.size,也就是说,无论你传入多大的 size,都不影响返回的结果。当然,也有子类重写了 sizeThatFits,导致它们的行为表现得不同的,比如 UISwitch 会返回苹果预设的尺寸 (51.0, 31.0),而 UIButton 和 UIImageView 都是根据自己的内容大小返回 size。

UILabel 比较特殊,如果 numberOfLines 大于 1,sizeThatFits 会依据传入的 size.width 在限定的 lines 中,计算出 size;如果 numberOfLines 等于 1,传入的 size 没用。

而 sizeToFit 的默认实现则会调用 sizeThatFits,并将调用者的 size 调整为 sizeThatFits 返回的 size 大小,不影响 origin。通常来说我们会通过这种方式来设置 label 的 size。

4,算高

其实算高大部分情况下都是计算多行文本的高度,对于 frame 布局,一般来说有两种方法。

第一种是用 NSString 的 api:

func boundingRect(with size: CGSize, options: NSStringDrawingOptions = []
, context: NSStringDrawingContext?) -> CGRect

调用此方法需要注意三点:

  • size 通常传固定宽度,高度为 CGFloat_MAX;
  • 对于多行文本,options 传 usesLineFragmentOrigin;
  • 返回值用 ceil 向上取整。

第二种是用 sizeThatFits 方法来计算,用传值和上面第一个值一样。但是这种方法必须有视图介入,会增加一些成本。

对于 autolayout 来说,系统提供了下面这个方法通过子视图的约束来自动计算:

func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize

使用这个方法要注意三点:

  • targetSize 一般传固定值 UILayoutFittingCompressedSize 来约束。
  • 对于算高来说,子视图必须在垂直方向上的约束拉通,在水平方向固定宽度。通常对于 UILabel 来说,设置其 preferredMaxLayoutWidth 来告诉系统自身的最大宽度就行了。

5,坐标转换

我们常常需要将一个位置从一个坐标系转换到另一个坐标系,用系统提供的 api,我们可以轻易的做到这一点,值得注意的是,用于转换的两个 view 必须在同一个 window 下。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.