NotAutoLayout
1布局框架,将布局人类错误2视为编译时错误-
-唯一的NotAutoLayout 是一个框架,可以帮助您在不使用 Auto Layout 约束的情况下对子视图进行布局。
请注意,此框架尚未广泛测试,目前仅适用于 iOS。
如果您遇到任何问题,有更好的建议,或者有其他想要告诉我的事情,请提出问题或发送我拉取请求。
为什么选择 NotAutoLayout
苹果推出了 Interface Builder 和 Storyboard 以帮助开发者以可视方式创建视图,这是一个非常好的主意。但自从 iPhone 5 以来,事情发生了变化。屏幕分辨率越来越多,纵横比各不相同,让开发者们感到疯狂。为了解决这个问题,苹果引入了 Auto Layout 和 Size Classes。
但问题是,它们只是使事情变得更加复杂(也许这就是为什么它们引入了 UIStackView 和 NSGridView)。您必须在故事板中创建数十个约束,只是为了创建一个非常简单的视图,这使得系统变得复杂且难以调试。此外,没有约束的层次结构,这意味着您必须在一个地方管理所有这些数十个约束,这并不直观,因为我们总是会给视图添加许多子视图,再给子视图添加更多子视图,从而形成视图的层次结构。
因此,存在一些框架,例如 Snapkit 和 PureLayout,可以帮助您更轻松地创建自动布局约束(这应该是 Xcode 的职责)。但是,仍然有一些 传统 的开发者(就像我一样 :))不喜欢,甚至讨厌自动布局,希望通过代码进行布局。这就是为什么我创建了 NotAutoLayout 框架的原因。
使用 NotAutoLayout,您不必关心任何像约束或尺寸类这样的东西,只需关注元素的边缘在哪里,我应该将元素调整到多大的大小,以及布局过程完成后可能需要做什么。通过 NotAutoLayout 框架完成的每个布局过程都是直接设置 frame
属性(如果 transform
是 .identity
),或者否则设置 bounds.size
和 center
属性,这使得其比约束更容易调试(是的,您甚至可以放一个断点来获取详细的布局信息!)。
需求
- iOS 9.0+
- Xcode 10.0+
- Swift 4.2+
安装
使用 Carthage
github "el-hoshino/NotAutoLayout" ~> 3.2
- 运行
carthage update
。 - 将框架添加到您的项目中。
使用 CocoaPods
pod 'NotAutoLayout', `~> 3.2`
- 运行
pod install
。
手动
- 下载整个仓库。
- 将 NotAutoLayout 项目文件添加到您的项目中。
- 构建框架。
使用方法
快速了解
我编写了一个基本可以实际应用的示例代码代码块文件,这可能有助于您理解。以下是主要源代码,您可以在项目内部找到完整的示例代码。
import PlaygroundSupport
import NotAutoLayout
let controller = IPhoneXScreenController()
controller.rotate(to: .portrait)
PlaygroundPage.current.liveView = controller.view
let appView = LayoutInfoStoredView()
appView.backgroundColor = .white
controller.view.addSubview(appView)
controller.view.nal.layout(appView) { $0
.stickOnParent()
}
let padding: NotAutoLayout.Float = 10
let summaryView = ProfileSummaryView()
let contentsView = ContentsView()
let replyView = ReplyView()
summaryView.avatar = #imageLiteral(resourceName: "avatar.png")
summaryView.mainTitle = "星野恵瑠@今日も1日フレンズ㌠"
summaryView.subTitle = "@lovee"
contentsView.contents = """
Hello, NotAutoLayout!
The new NotAutoLayout 3.0 now has even better syntax which is:
- Easy to write (thanks to method-chain structures)
- Easy to read (thanks to more natural English-like statements)
- Capable with dynamic view sizes (with commands like `fitSize()`)
- Even better closure support to help you set various kinds of layouts with various kinds of properties!
- And one more thing: an experienmental sequencial layout engine!
"""
contentsView.timeStamp = Date()
appView.nal.setupSubview(summaryView) { $0
.setDefaultLayout { $0
.setLeft(by: { $0.layoutMarginsGuide.left })
.setRight(by: { $0.layoutMarginsGuide.right })
.setTop(by: { $0.layoutMarginsGuide.top })
.setHeight(to: 50)
}
.addToParent()
}
appView.nal.setupSubview(contentsView) { $0
.setDefaultLayout({ $0
.setLeft(by: { $0.layoutMarginsGuide.left })
.setRight(by: { $0.layoutMarginsGuide.right })
.pinTop(to: summaryView, with: { $0.bottom + padding })
.fitHeight()
})
.setDefaultOrder(to: 1)
.addToParent()
}
appView.nal.setupSubview(replyView) { $0
.setDefaultLayout({ $0
.setBottomLeft(by: { $0.layoutMarginsGuide.bottomLeft })
.setRight(by: { $0.layoutMarginsGuide.right })
.setHeight(to: 30)
})
.addToParent()
}
let imageViews = (0 ..< 3).map { (_) -> UIImageView in
let image = #imageLiteral(resourceName: "avatar.png")
let view = UIImageView(image: image)
appView.addSubview(view)
return view
}
appView.nal.layout(imageViews) { $0
.setMiddle(by: { $0.vertical(at: 0.7) })
.fitSize()
.setHorizontalInsetsEqualingToMargin()
}
了解更多
NotAutoLayout 的基本方法是为每个视图创建一个 CGRect
,以便可以应用其 frame
属性(或 bound.size
和 center
属性,如果 transform
属性不是 .identity
)。在 NotAutoLayout 中有很多种创建 CGRect
的方法,不仅限于 x
、y
、width
、height
,您还可以指定诸如 center
、bottomtLeft
、middleRight
等内容。
基本上,您有 4 种方法使用 NotAutoLayout
- 继承自
UIView
(或任何其他现存UIView
的子类)并重写layoutSubviews()
- 直接使用
LayoutInfoStoredView
- 继承自
LayoutInfoStoredView
- 继承自
UIView
(或任何其他现存UIView
的子类)并实现LayoutInfoStorable
协议
对于第 1 种方法,您只需在 layoutSubviews()
方法中编写您的布局代码即可;对于其他 3 种方法,您可以为每个视图设置一个布局,视图将在 layoutSubviews()
方法中自动处理布局计算,因此您不需要重写它。
在 layoutSubviews()
方法中布局子视图,可以使用如下代码
class MyView: UIView {
let viewA = UIView()
let viewB = UIView()
override func layoutSubviews() {
super.layoutSubviews()
self.nal.layout(self.viewA) { $0
.setTopCenter(by: { $0.topCenter })
.setWidth(by: { $0.width })
.fitHeight()
}
self.nal.layout(self.viewB) { $0
.pinTopCenter(to: self.viewA, with: { $0.bottomCenter })
.setWidth(by: { $0.width })
.fitHeight()
}
}
}
这将为 viewA
的顶部中心布局到其父视图的顶部中心(即 MyView
的顶部中心),具有与其父视图相同的宽度(即 MyView
),然后自动适应其高度,接着将 viewB
的顶部中心布局到 viewA
的顶部底部,具有与其父视图相同的宽度(即 MyView
),然后自动适应其高度。
要为子视图设置布局,父视图必须符合 LayoutInfoStorable
协议,您也可以使用如下代码
class ViewController: UIViewController {
private(set) lazy var mainView = LayoutInfoStoredView()
private(set) lazy var viewA = UIView()
private(set) lazy var viewB = UIView()
override func loadView() {
self.mainView.frame = UIScreen.main.bounds
self.view = self.mainView
}
override func viewDidLoad() {
super.viewDidLoad()
self.mainView.nal.setLayout(for: self.viewA) { $0
.setTopCenter(by: { $0.topCenter })
.setWidth(by: { $0.width })
.fitHeight()
}
self.mainView.nal.setupSubview(self.viewB) { $0
.setDefaultLayout({ $0
.pinTopCenter(to: self.viewA, with: { $0.bottomCenter })
.setWidth(by: { $0.width })
.fitHeight()
})
.setDefaultOrder(to: 1)
}
}
}
这与之前的代码做相同的事情,这种方式的好处是您不需要为生成主视图mainView
对UIView
进行子类化。但是,由于您的viewB
的布局依赖于viewA
,您需要设置viewB
的布局顺序来确保它会先于viewA
进行布局。
如何实现布局
您可以通过调用nal.layout(_ subview: UIView, by making: (_ layoutMaker: LayoutMaker<IndividualProperty.Initial>) -> LayoutMaker<IndividualLayout>)
来从父视图设置子视图的布局。其基本思路是通过LayoutMaker
创建布局。通过LayoutMaker
,您可以分步设置框架的每个部分以生成特定的布局,然后父视图将使用生成的布局设置子视图的框架。
要使用LayoutMaker
设置布局,您可以在提供的making
闭包中调用方法,如$0.setTopLeft(to: .zero).fitSize()
。由于这是一个(LayoutMaker<IndividualProperty.Initial>) -> LayoutMaker<IndividualLayout>
类型的闭包,它将确保最后您将获得一个特定的布局,避免了Auto Layout中的模糊布局等问题。并且因为LayoutMaker<IndividualProperty.Initial>
有许多导致LayoutMaker<IndividualLayout>
的方法链,您可以轻松地编写一个方法链而不会遗漏框架的任何部分。
从理论上讲,要创建一个框架,您需要4个元素:2个独特的水平位置和2个独特的垂直位置。水平位置可能是像left
、center
、right
或任何特定位置如0.3
,以及width
,这也代表了一个水平位置。垂直位置可能是像top
、middle
、bottom
或任何特定位置如0.3
,当然也包括height
。目前,因为有些LayoutProperty
尚未声明,通过LayoutMaker
设置这些元素时,有一个近似的顺序:
- 设置具体点位置(如
.setTopLeft
)在设置线路位置(如.setTop
或.setLeft
)之前。 - 在设置尺寸元素(
.setWidth
、.setHeight
和.setSize
)之前设置线路位置。 - 在线路位置中,先设置水平位置(如
.setLeft
)再设置垂直位置(如.setTop
)。
由于您只需要2个独特的水平位置和2个独特的垂直位置来创建一个框架,您不需要编写这4种类别的所有语句。例如,您可以通过编写$0.setTopLeft(to: .zero).fitSize()
来创建一个特定的框架,它只有2个语句。
在LayoutMaker
中,您还有一些方法可以获取父视图的大小和与安全区域相关的属性(如.setLeft(by: { $0.safeAreaGuide.topCenter })
),这可以帮助您创建响应式布局。
它是如何实际工作的
NotAutoLayout为用户提供了多个API来为子视图创建特定框架,例如pinTopCenter
等先前介绍过的。作为一个开发者,你只需要关注设计师如何设计每个子视图的布局,然后将它直接转化成代码。NotAutoLayout的布局引擎将负责生成准确的CGRect
。
为了提供创建框架的API,NotAutoLayout包含了许多LayoutProperty
。通过这些生成器,你在编写代码时可以轻松理解构建特定框架所需哪些元素,并容易理解开发者通过代码想要实现的布局。这些生成器的初始化器对开发者隐藏起来,因此要访问它,需要使用类似.nal.layout(aSubView)
的代码,然后在下面的闭包中会得到一个生成器,如### 了解更多
中的示例代码所示。示例代码中的$0
代表布局生成器。通过这些链式方法,如pinTopCenter
,你最终在闭包中得到的是LayoutMaker<IndividualLayout>
。当闭包计算完成后,NotAutoLayout的布局引擎会从IndividualLayout
中提取布局。
与其他布局框架的比较
项目 | NotAutoLayout | PinLayout | LayoutKit | SnapKit | PureLayout | Cartography |
---|---|---|---|---|---|---|
编写语言 | Swift | Swift | Swift(带少量ObjC) | Swift | ObjC(带少量Swift) | Swift |
基于 | 纯代码 | 纯代码 | 纯代码 | 自动布局 | 自动布局 | 自动布局 |
平台 | iOS | iOS / macOS / tvOS | iOS / macOS / tvOS | iOS / macOS / tvOS | iOS / macOS / tvOS | iOS / macOS |
依赖管理器 | CocoaPods / Carthage | CocoaPods / Carthage | CocoaPods / Carthage | CocoaPods / Carthage | CocoaPods / Carthage | CocoaPods / Carthage |
性能 | ◯ | ◯ | ◯ | × | × | × |
易于编写 | ◯ | ◯ | × | ◯ | ◯ | △3 |
易于阅读 | ◯ | △4 | △5 | ◯ | ◯ | ◯ |
Ex-Short syntax | × | ◯ | × | × | × | × |
链式方法 | ◯ | ◯ | × | △6 | × | × |
目标扩展7 | ◯ | ◯ | ×8 | ◯ | × | ×8 |
人类错误的行为2 | 构建时错误 | 运行时警告 | 构建时错误9 | 运行时警告 | 运行时警告 | 运行时警告 |
已知的已知问题
- 内联文档尚未完成。
- 一些
LayoutProperty
尚未声明。 - 矩阵布局尚未实现。
许可
NotAutoLayout在Apache许可证下发布。有关详细信息,请参阅LICENSE。
注释
- 1: 可能吧。至少我个人还没有找到第二个。
- 2: 比如自动布局中的模糊布局,或者在重写
layoutSubviews
时忘记设置子视图的边缘。 - 3: 语法不是很难记忆,但是你必须编写全局函数而不是方法,这使得与方法相比,自动完成会稍微困难一些。
- 4: 有时候方法名太短,理解它真正做什么会有些困难。
- 5: 并不难理解,但阅读这些语法仍然需要一些时间。
- 6: SnapKit 有链式方法来帮助您制作 Auto Layout 约束,但您可能仍然需要编写多个语句才能实现所有必需的约束。
- 7: 以下是类似这样命名的命名空间扩展方法的一个例子。一个著名的例子是 RxSwift 中的
.rx
访问控制。 - 8: LayoutKit 和 Cartography 没有命名空间扩展机制,但 LayoutKit 使用局部类型,而 Cartography 使用函数来避免潜在的名称冲突。
- 9: 从理论上讲,LayoutKit 在其语法中不会创建任何模糊布局,但它仍然会产生一些隐式尺寸过程。