FixFlex Logo

FixFlex 是一个基于 NSLayoutAnchor API 构建的简单而强大的 Auto Layout 库,是对 Visual Format Language 的一种 Swift 化的类型安全重新构想。

特性

用法

假设我们想要创建像这样的布局

  1. 让我们水平扫描布局并将其转换为 FixFlex 代码

大多数视图和间距具有固定的宽度 (Fix),而标题和副标题的宽度是弹性的,旨在占据剩余空间 (Flex)

parent.fx.hstack(Fix(15),
                 Fix(iconView, 44),
                 Fix(15),
                 Flex([titleLabel, subtitleLabel]),
                 Fix(15),
                 Fix(chevron, 20),
                 Fix(15))
  1. 垂直方向上,我们有三个不同的视图组。从图标开始

我们使用 Fix 在顶部进行间距设置。底部间距应至少为 15pt,以应对标签高度小于图标高度的情况

parent.fx.vstack(Fix(15),
                 Fix(iconView, 44),
                 Flex(min: 15))
  1. 接下来,我们对标题和副标题进行垂直扫描

parent.fx.vstack(Fix(15),
                 Flex(titleLabel),
                 Flex(subtitleLabel),
                 Fix(15))
  1. 最后,我们垂直扫描 chevron

为了使 chevron 居中,我们使用 Fill 确保顶部间距等于底部间距

parent.fx.vstack(Fill(),
                 Fix(chevron, 30),
                 Fill())

就是这样! 最好的部分是 FixFlex 布局代码非常容易修改,可以轻松插入额外的填充或视图,而无需重新连接约束。

API

hstack/vstack

FixFlex 提供了两个函数用于水平 (hstack) 和垂直 (vstack) 布局视图,可通过 view.fx.* 命名空间访问。

您可以指定 startAnchor/endAnchor 以在任意锚点之间而不是视图边缘之间布局项目。 startOffset/endOffset 用于分别从 startAnchorendAnchor 添加间距或偏移。

默认情况下,hstack 在自然定位模式下工作,并使用 leadingAnchor/trailingAnchor 进行操作。 此设置确保布局针对从右到左的语言进行镜像。 但是,可以通过启用 useAbsolutePositioning 标志来覆盖此行为。 当此标志设置为 true 时,hstack 会切换为使用 leftAnchor/rightAnchor 进行布局定位。

func hstack(
        startAnchor: NSLayoutXAxisAnchor? = nil, // if nil, we use leadingAnchor or leftAnchor
        startOffset: CGFloat = 0,
        endAnchor: NSLayoutXAxisAnchor? = nil, // if nil, we use trailingAnchor or rightAnchor
        endOffset: CGFloat = 0,
        useAbsolutePositioning: Bool = false, // if true, we use leftAnchor/rightAnchor based positioning (force Left-To-Right)
        _ intents: SizingIntent...
    ) -> StackingResult
func vstack(
        startAnchor: NSLayoutYAxisAnchor? = nil, // if nil, we use topAnchor
        startOffset: CGFloat = 0,
        endAnchor: NSLayoutYAxisAnchor? = nil, // if nil, we use bottomAnchor
        endOffset: CGFloat = 0,
        _ intents: SizingIntent...
    ) -> StackingResult

SizingIntent 本质上是用于计算宽度或高度的指令

SizingIntent 的具体实例可以使用专门的构建器函数创建

Fix

用于指定视图/spacer 的确切大小。

func Fix(_ value: CGFloat) -> SizingIntent

func Fix(_ view: _View, _ value: CGFloat) -> SizingIntent

func Fix(_ views: [_View], _ value: CGFloat) -> SizingIntent

Flex

适用于动态变化的大小。 可选地,可以为 hugging 和 compression resistance 指定最小/最大约束和就地优先级设置。

func Flex(min: CGFloat? = nil, max: CGFloat? = nil) -> SizingIntent

func Flex(_ view: _View, min: CGFloat? = nil, max: CGFloat? = nil, huggingPriority: _LayoutPriority? = nil, compressionResistancePriority: _LayoutPriority? = nil) -> SizingIntent

func Flex(_ views: [_View], min: CGFloat? = nil, max: CGFloat? = nil, huggingPriority: _LayoutPriority? = nil, compressionResistancePriority: _LayoutPriority? = nil) -> SizingIntent

Fill

Fill 允许视图/spacer 根据其权重成比例地占据可用的自由空间。 它特别适用于实现相等的间距、居中元素或设计对称布局(如表格或网格)。

func Fill(weight: CGFloat = 1.0) -> SizingIntent

func Fill(_ view: _View, weight: CGFloat = 1.0) -> SizingIntent

func Fill(_ views: [_View], weight: CGFloat = 1.0) -> SizingIntent

Match

这用于将视图或 spacer 的大小与指定的 NSLayoutDimension 相匹配。 它特别适用于对齐不同视图或 spacer 的大小,或使其大小彼此成比例。

public func Match(dimension: NSLayoutDimension, multiplier: CGFloat? = nil, offset: CGFloat? = nil) -> SizingIntent

public func Match(_ view: _View, dimension: NSLayoutDimension, multiplier: CGFloat? = nil, offset: CGFloat? = nil) -> SizingIntent

public func Match(_ views: [_View], dimension: NSLayoutDimension, multiplier: CGFloat? = nil, offset: CGFloat? = nil) -> SizingIntent

工作原理

FixFlex 不是一个黑盒,也不使用任何魔法。 它只是一种声明式且方便的方式来创建约束和布局指南。 让我们看一下当我们想要垂直居中两个标签时,FixFlex 如何转换为标准 Auto Layout 调用

parent.fx.hstack(Flex([topLabel, bottomLabel]))

parent.fx.vstack(Fill(),
                 Flex(topLabel),
                 Fix(5),
                 Flex(bottomLabel),
                 Fill())

在底层,FixFlex 创建的约束和布局指南等同于以下内容

topLabel.translatesAutoresizingMaskIntoConstraints = false
bottomLabel.translatesAutoresizingMaskIntoConstraints = false

let layoutGuideTop = UILayoutGuide()
let layoutGuideMiddle = UILayoutGuide()
let layoutGuideBottom = UILayoutGuide()

parent.addLayoutGuide(layoutGuideTop)
parent.addLayoutGuide(layoutGuideMiddle)
parent.addLayoutGuide(layoutGuideBottom)

NSLayoutConstraint.activate([
    // hstack
    topLabel.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
    topLabel.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
    bottomLabel.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
    bottomLabel.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
    //vstack
    layoutGuideTop.topAnchor.constraint(equalTo: parent.topAnchor),
    layoutGuideTop.bottomAnchor.constraint(equalTo: topLabel.topAnchor),
    topLabel.bottomAnchor.constraint(equalTo: layoutGuideMiddle.topAnchor),
    layoutGuideMiddle.heightAnchor.constraint(equalToConstant: 5),
    layoutGuideMiddle.bottomAnchor.constraint(equalTo: bottomLabel.topAnchor),
    bottomLabel.bottomAnchor.constraint(equalTo: layoutGuideBottom.topAnchor),
    layoutGuideBottom.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
    layoutGuideTop.heightAnchor.constraint(equalTo: layoutGuideBottom.heightAnchor),
])

哎呀,要写这么多代码,想象一下需要修改它——插入一个额外的视图或更改顺序。 一旦你尝试了 FixFlex,你就不会想回去了!

示例

使用 Inset 填充父视图

parent.fx.hstack(Fix(15),
                 Flex(child),
                 Fix(15))

parent.fx.vstack(Fix(15),
                 Flex(child),
                 Fix(15))

Pin 到父视图 Trailing Bottom

parent.fx.hstack(Flex(),
                 Fix(child, 100),
                 Fix(15))

parent.fx.vstack(Flex(),
                 Fix(child, 50),
                 Fix(15))

在父视图中居中

parent.fx.hstack(Fill(),
                 Fix(child, 100),
                 Fill())

parent.fx.vstack(Fill(),
                 Fix(child, 50),
                 Fill())

在父视图中居中标签

parent.fx.hstack(Fill(),
                 Flex(label),
                 Fill())

parent.fx.vstack(Fill(),
                 Flex(label),
                 Fill())

垂直居中两个标签

parent.fx.hstack(Flex([topLabel, bottomLabel]))

parent.fx.vstack(Fill(),
                 Flex(topLabel),
                 Fix(5),
                 Flex(bottomLabel),
                 Fill())

带有图标标题副标题和 Chevron 的单元格

parent.fx.hstack(Fix(15),
                 Fix(iconView, 44),
                 Fix(15),
                 Flex([titleLabel, subtitleLabel]),
                 Fix(15),
                 Fix(chevron, 20),
                 Fix(15))

parent.fx.vstack(Fix(15),
                 Fix(iconView, 44),
                 Flex(min: 15))

parent.fx.vstack(Fix(15),
                 Flex(titleLabel),
                 Flex(subtitleLabel),
                 Fix(15))

parent.fx.vstack(Fill(),
                 Fix(chevron, 30),
                 Fill())

带有图标标题和副标题的卡片

parent.fx.hstack(Fix(5),
                 Flex([iconView, titleLabel, subtitleLabel]),
                 Fix(5))

parent.fx.vstack(Fix(5),
                 Fix(iconView, 50),
                 Fix(10),
                 Flex(titleLabel),
                 Flex(subtitleLabel),
                 Fix(5))

标签行空间不足以容纳两者

parent.fx.vstack(Flex([leftLabel, rightLabel]))

parent.fx.hstack(Flex(leftLabel, compressionResistancePriority: .required),
                 Fix(5),
                 Flex(rightLabel))

标签拆分

parent.fx.vstack(Fix(5),
                 Flex([label1, label2, label3]),
                 Fix(5))

parent.fx.hstack(Fix(5),
                 Fill(label1, weight: 2),
                 Fix(5),
                 Fill(label2),
                 Fix(5),
                 Fill(label3),
                 Fix(5))

Flex 最小最大值

parent.fx.vstack(Fix(5),
                 Flex(label1),
                 Flex(label2),
                 Flex(label3),
                 Fix(5))

parent.fx.hstack(Fix(5),
                 Flex(label1),
                 Flex(),
                 Fix(5))

parent.fx.hstack(Fix(5),
                 Flex(label2, min: 175),
                 Flex(),
                 Fix(5))

parent.fx.hstack(Fix(5),
                 Flex(label3, max: 100),
                 Flex(),
                 Fix(5))

放置在锚点之间

parent.fx.vstack(Flex([label, leadingView, trailingView]))

parent.fx.hstack(Fill(),
                 Flex(label),
                 Fill())

parent.fx.hstack(startAnchor: label.leadingAnchor,
                 endAnchor: label.trailingAnchor,
                 Fix(leadingView, 20),
                 Flex(),
                 Fix(trailingView, 20))

放置在绝对锚点之间

parent.fx.vstack(Flex([label, leadingView, trailingView]))

parent.fx.hstack(Fill(),
                 Flex(label),
                 Fill())

parent.fx.hstack(startAnchor: label.leftAnchor,
                 endAnchor: label.rightAnchor,
                 useAbsolutePositioning: true,
                 Fix(leadingView, 20),
                 Flex(),
                 Fix(trailingView, 20))

使用 Match 的阴影

parent.fx.vstack(Fill(),
                 Flex(label),
                 Fill())

parent.fx.hstack(Fill(),
                 Flex(label),
                 Fill())

parent.fx.vstack(startAnchor: label.topAnchor,
                 Fix(10),
                 Match(matchView, dimension: label.heightAnchor),
                 Flex())

parent.fx.hstack(startAnchor: label.leadingAnchor,
                 Fix(10),
                 Match(matchView, dimension: label.widthAnchor),
                 Flex())

集成

Swift Package Manager

使用 Swift Package Manager 并将依赖项添加到 Package.swift 文件。

  dependencies: [
    .package(url: "https://github.com/psharanda/FixFlex.git", .upToNextMajor(from: "1.0.0"))
  ]

或者,在 Xcode 中选择 File > Add Package Dependencies… 并添加 FixFlex 存储库 URL

https://github.com/psharanda/FixFlex.git

Carthage

github "psharanda/FixFlex" 添加到你的 Cartfile

CocoaPods

FixFlex 可通过 CocoaPods 获得。 要安装它,只需将以下行添加到你的 Podfile

pod "FixFlex"

贡献

我们欢迎贡献! 如果您发现错误、有功能请求或想贡献代码,请打开一个 issue 或提交一个 pull request。

许可

FixFlex 在 MIT 许可证下可用。 有关更多信息,请参阅 LICENSE 文件。