FixFlex 是一个简单而强大的基于 NSLayoutAnchor API 的 Auto Layout 库,是对 视觉格式语言的轻量级和类型安全的重新想象
- 声明式 Auto Layout 代码,易于编写、阅读和修改
- 简单的 API,包含两个函数和四个规范,涵盖 99% 的布局用例
- 实现代码仅为 300 行
- 与任何其他 Auto Layout 代码兼容
- 基本生成一系列激活的
NSLayoutConstraint和UILayoutGuide - 保持视图层次结构平面,无需额外容器
- 轻量级的
UIStackView替代方案 - 非常直观的思维模型
- 对 VFL 的类型安全替代方案
- 动态类型和从右到左友好
- 自动将
translatesAutoresizingMaskIntoConstraints设置为 false - 支持 iOS 12.0+ / Mac OS X 10.13+ / tvOS 12.0+
想象我们想创建一个类似这样的布局
- 让我们水平扫描布局并将其转换为 FixFlex 代码
大多数视图和间隔具有固定宽度(Fix),而标题和副标题宽度是灵活的,设计为占据剩余空间(Flex)
parent.fx.hstack(Fix(15),
Fix(iconView, 44),
Fix(15),
Flex([titleLabel, subtitleLabel]),
Fix(15),
Fix(chevron, 20),
Fix(15))- 垂直方向上,我们有三个不同的视图组。从图标开始
我们使用 Fix 在顶部做间隔。底部的间隔至少应为 15pt,以防标签的高度小于图标的宽度
parent.fx.vstack(Fix(15),
Fix(iconView, 44),
Flex(min: 15))- 接下来,我们对标题和副标题进行垂直扫描
parent.fx.vstack(Fix(15),
Flex(titleLabel),
Flex(subtitleLabel),
Fix(15))- 最后,我们对箭头进行垂直扫描
为了居中箭头,我们确保顶部的间隔等于底部的间隔,使用 Fill
parent.fx.vstack(Fill(),
Fix(chevron, 30),
Fill())这就完成了!最好的部分是修改 FixFlex 布局代码有多么容易,可以轻松地插入额外的填充或视图,无需重新连接约束。
FixFlex 提供了两个函数用于水平(hstack)和垂直(vstack)布局,可以通过 view.fx.* 命名空间访问。
您可以指定 startAnchor/endAnchor,用于在任意锚点之间布局项目,而不是视图的边缘。使用 startOffset/endOffset 来添加从 startAnchor 和 endAnchor 的间距或偏移量。
默认情况下,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...
) -> StackingResultfunc 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...
) -> StackingResultSizingIntent 实质上是一组指令,用于计算以下内容的大小
- 一个间隔(后面在幕后创建了一个
UILayoutGuide) - 一个视图
- 观点的数组(当它们平行对齐时)
可以使用专用构建函数创建 SizingIntent 的具体实例。
用于指定视图/间距的确切大小。
func Fix(_ value: CGFloat) -> SizingIntent
func Fix(_ view: _View, _ value: CGFloat) -> SizingIntent
func Fix(_ views: [_View], _ value: CGFloat) -> SizingIntent适用于动态变化的大小。可选地,可以指定最小/最大约束以及拥抱和压缩抵抗的内部优先级设置。
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填充 允许视图/间距根据其权重按比例占用可用空闲空间。这对于实现等距、居中元素或设计对称布局(如表格或网格)特别有用。
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用于将视图或间距的大小匹配到指定的 NSLayoutDimension。这在调整不同视图或间距的大小,或使它们的大小成比例时特别有用。
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) -> SizingIntentFixFlex 不是一个黑盒,也不使用任何魔法。它只是一个声明性和方便的方式,用于创建约束和布局指南。让我们看看当我们要垂直居中两个标签时,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,你就不会想要回去了!
parent.fx.hstack(Fix(15),
Flex(child),
Fix(15))
parent.fx.vstack(Fix(15),
Flex(child),
Fix(15))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())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))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))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.swift 文件中。
dependencies: [
.package(url: "https://github.com/psharanda/FixFlex.git", .upToNextMajor(from: "1.0.0"))
]或者,在 Xcode 中选择 文件 > 添加包依赖... 并添加 FixFlex 存储库 URL
https://github.com/psharanda/FixFlex.git
我们欢迎贡献!如果您发现了一个错误,有一个功能请求,或想贡献代码,请打开一个问题或提交一个拉取请求。
FixFlex 可在 MIT 许可证下获得。有关更多信息,请参阅 LICENSE 文件。















