Skip to content

Instantly share code, notes, and snippets.

@Josscii
Last active June 7, 2018 10:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Josscii/3ba6b9abb3d0c0936f4fd68ab39454f5 to your computer and use it in GitHub Desktop.
Save Josscii/3ba6b9abb3d0c0936f4fd68ab39454f5 to your computer and use it in GitHub Desktop.
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