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...
) -> 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
实质上是一组指令,用于计算以下内容的大小
- 一个间隔(后面在幕后创建了一个
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) -> 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,你就不会想要回去了!
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 文件。