NotAutoLayout 3.2.0

NotAutoLayout 3.2.0

史翔新 维护。




NotAutoLayout

Platform Language Carthage compatible Build Status

-唯一的1布局框架,将布局人类错误2视为编译时错误-

NotAutoLayout 是一个框架,可以帮助您在不使用 Auto Layout 约束的情况下对子视图进行布局。

请注意,此框架尚未广泛测试,目前仅适用于 iOS。

如果您遇到任何问题,有更好的建议,或者有其他想要告诉我的事情,请提出问题或发送我拉取请求。

为什么选择 NotAutoLayout

苹果推出了 Interface Builder 和 Storyboard 以帮助开发者以可视方式创建视图,这是一个非常好的主意。但自从 iPhone 5 以来,事情发生了变化。屏幕分辨率越来越多,纵横比各不相同,让开发者们感到疯狂。为了解决这个问题,苹果引入了 Auto Layout 和 Size Classes。

但问题是,它们只是使事情变得更加复杂(也许这就是为什么它们引入了 UIStackView 和 NSGridView)。您必须在故事板中创建数十个约束,只是为了创建一个非常简单的视图,这使得系统变得复杂且难以调试。此外,没有约束的层次结构,这意味着您必须在一个地方管理所有这些数十个约束,这并不直观,因为我们总是会给视图添加许多子视图,再给子视图添加更多子视图,从而形成视图的层次结构。

因此,存在一些框架,例如 SnapkitPureLayout,可以帮助您更轻松地创建自动布局约束(这应该是 Xcode 的职责)。但是,仍然有一些 传统 的开发者(就像我一样 :))不喜欢,甚至讨厌自动布局,希望通过代码进行布局。这就是为什么我创建了 NotAutoLayout 框架的原因。

使用 NotAutoLayout,您不必关心任何像约束或尺寸类这样的东西,只需关注元素的边缘在哪里,我应该将元素调整到多大的大小,以及布局过程完成后可能需要做什么。通过 NotAutoLayout 框架完成的每个布局过程都是直接设置 frame 属性(如果 transform.identity),或者否则设置 bounds.sizecenter 属性,这使得其比约束更容易调试(是的,您甚至可以放一个断点来获取详细的布局信息!)。

需求

  • iOS 9.0+
  • Xcode 10.0+
  • Swift 4.2+

安装

使用 Carthage

github "el-hoshino/NotAutoLayout" ~> 3.2
  • 运行 carthage update
  • 将框架添加到您的项目中。

使用 CocoaPods

  • 下载并安装 CocoaPods
  • 在您的 Podfile 中指定 NotAutoLayout,如下所示
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()
}

screenshot

了解更多

NotAutoLayout 的基本方法是为每个视图创建一个 CGRect,以便可以应用其 frame 属性(或 bound.sizecenter 属性,如果 transform 属性不是 .identity)。在 NotAutoLayout 中有很多种创建 CGRect 的方法,不仅限于 xywidthheight,您还可以指定诸如 centerbottomtLeftmiddleRight 等内容。

基本上,您有 4 种方法使用 NotAutoLayout

  1. 继承自 UIView(或任何其他现存 UIView 的子类)并重写 layoutSubviews()
  2. 直接使用 LayoutInfoStoredView
  3. 继承自 LayoutInfoStoredView
  4. 继承自 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)
		}
		
	}
	
}

这与之前的代码做相同的事情,这种方式的好处是您不需要为生成主视图mainViewUIView进行子类化。但是,由于您的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个独特的垂直位置。水平位置可能是像leftcenterright或任何特定位置如0.3,以及width,这也代表了一个水平位置。垂直位置可能是像topmiddlebottom或任何特定位置如0.3,当然也包括height。目前,因为有些LayoutProperty尚未声明,通过LayoutMaker设置这些元素时,有一个近似的顺序:

  1. 设置具体点位置(如.setTopLeft)在设置线路位置(如.setTop.setLeft)之前。
  2. 在设置尺寸元素(.setWidth.setHeight.setSize)之前设置线路位置。
  3. 在线路位置中,先设置水平位置(如.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 在其语法中不会创建任何模糊布局,但它仍然会产生一些隐式尺寸过程。