Epoxy 是一套用于在 Swift 中构建 UIKit 应用的声明式 UI API。Epoxy 受 Android 上的出色 Epoxy 框架 以及 Swift 中的其他声明式 UI 框架(如 SwiftUI )的启发和影响。
Epoxy 是在 Airbnb 开发的,并为数以千计的应用程序中的成千上万个屏幕提供动力。它已经由 数十位贡献者 开发和改进多年。
以下是我们使用 Epoxy 构建的 Airbnb 应用程序中的一些示例屏幕。我们对 Epoxy 的使用从最简单和静态屏幕到最先进和动态功能。
目录
安装
Epoxy 可以使用 CocoaPods 或 Swift 包管理器 进行安装。
CocoaPods
要开始使用使用 CocoaPods 的 Epoxy,请在您的 Podfile
中添加以下内容,然后按照集成说明 操作。
Epoxy 被分割成了针对每个 模块 的 Podspec ,因此您只需包含所需的部分。
Swift Package Manager (SPM)
要使用 Swift Package Manager 安装 Epoxy,您可以参考 Apple 发布的教程 ,并使用 Epoxy 仓库当前版本的 URL。
在 Xcode 中,选择“文件”→“Swift Packages”→“添加包依赖”
输入 https://github.com/airbnb/epoxy-ios.git
Epoxy 为每个 模块 分割成 库产品 ,您只需包含所需的即可。
模块
Epoxy 采用模块化架构,因此您只需包含针对您用例所需的模块。
文档和教程
有关完整文档和分步教程,请参阅 wiki 。有关类型级别的文档,请参阅托管在 SwiftPackageIndex 上的 Epoxy DocC 文档 。
还提供了一个完整的示例应用程序,其中包含许多示例。您可以运行它通过 EpoxyExample
方案在 Epoxy.xcworkspace
中,或者浏览其 源代码 。
如果您还有疑问,请随时创建一个新 问题 。
入门
EpoxyCollectionView
EpoxyCollectionView
提供了一个声明式 API 来驱动 UICollectionView
的内容。它是可子化的 UIViewController
的一个子类,允许您使用声明式 API 简单地创建由 UICollectionView
支持的视图控制器。
下面的代码示例将渲染一个包含在 UICollectionView
中的单蜂窝,该细胞包含在细胞中渲染的 TextRow
组件。`TextRow` 是一个简单的 UIView
,包含两个标签,它符合EpoxyableView
协议。
您可以直接使用部分实例化一个 CollectionViewController
实例,例如以下带有可选行的视图控制器
源 结果
enum DataID {
case row
}
let viewController = CollectionViewController (
layout : UICollectionViewCompositionalLayout
.list (using : .init (appearance : .plain )),
items : {
TextRow.itemModel (
dataID : DataID.row ,
content : .init (title : " Tap me!" ),
style : .small )
.didSelect { _ in
// Handle selection
}
})
或者,您可以对该视图控制器进行子类化以达到更高级的情况,例如以下跟踪运行计数的视图控制器
源 结果
class CounterViewController : CollectionViewController {
init () {
let layout = UICollectionViewCompositionalLayout
.list (using : .init (appearance : .plain ))
super .init (layout : layout)
setItems (items, animated : false )
}
enum DataID {
case row
}
var count = 0 {
didSet {
setItems (items, animated : true )
}
}
@ItemModelBuilder
var items: [ItemModeling] {
TextRow.itemModel (
dataID : DataID.row ,
content : .init (
title : " Count \( count ) " ,
body : " Tap to increment" ),
style : .large )
.didSelect { [weak self ] _ in
self ? .count += 1
}
}
}
您可以在其 维基条目 或通过浏览 代码文档 来了解有关 EpoxyCollectionView
的更多信息。
EpoxyBars
EpoxyBars
提供了一个声明式 API 来渲染固定顶部、固定底部,或 输入辅助 吧堆栈在 UIViewController
中。
以下代码示例将渲染固定在 UIViewController
视图底部的 Button
组件。`ButtonRow` 是一个简单的包含单个 `UIButton` 组件的 UIView
,该按钮约束到父视图的边距,它符合 EpoxyableView
协议。
源 结果
class BottomButtonViewController : UIViewController {
override func viewDidLoad () {
super .viewDidLoad ()
bottomBarInstaller.install ()
}
lazy var bottomBarInstaller = BottomBarInstaller (
viewController : self ,
bars : bars)
@BarModelBuilder
var bars: [BarModeling] {
ButtonRow.barModel (
content : .init (text : " Click me!" ),
behaviors : .init (didTap : {
// Handle button selection
}))
}
}
您可以在其 维基条目 或通过浏览 代码文档 来了解有关 EpoxyBars
的更多信息。
EpoxyNavigationController
EpoxyNavigationController
提供了一个声明式接口来驱动 UINavigationController
的导航堆栈。
以下代码示例展示了您如何使用它轻松驱动一个具有多个视图控制器流程的功能。
源 结果
class FormNavigationController : NavigationController {
init () {
super .init ()
setStack (stack, animated : false )
}
enum DataID {
case step1 , step2
}
var showStep2 = false {
didSet {
setStack (stack, animated : true )
}
}
@NavigationModelBuilder
var stack: [NavigationModel] {
.root (dataID : DataID.step1 ) { [weak self ] in
Step1ViewController (didTapNext : {
self ? .showStep2 = true
})
}
if showStep2 {
NavigationModel (
dataID : DataID.step2 ,
makeViewController : {
Step2ViewController (didTapNext : {
// Navigate away from this step.
})
},
remove : { [weak self ] in
self ? .showStep2 = false
})
}
}
}
您可以在其 维基条目 中了解更多关于 EpoxyNavigationController
的信息,或者通过浏览 代码文档 。
EpoxyPresentations
EpoxyPresentations
提供了一个声明式接口来驱动 UIViewController
的模态呈现。
以下代码示例展示了您如何使用它轻松驱动一个在首次出现时显示模态的功能。
源 结果
class PresentationViewController : UIViewController {
override func viewDidAppear (_ animated : Bool ) {
super .viewDidAppear (animated)
setPresentation (presentation, animated : true )
}
enum DataID {
case detail
}
var showDetail = true {
didSet {
setPresentation (presentation, animated : true )
}
}
@PresentationModelBuilder
var presentation: PresentationModel? {
if showDetail {
PresentationModel (
dataID : DataID.detail ,
presentation : .system ,
makeViewController : { [weak self ] in
DetailViewController (didTapDismiss : {
self ? .showDetail = false
})
},
dismiss : { [weak self ] in
self ? .showDetail = false
})
}
}
}
您可以在其 维基条目 中了解更多关于 EpoxyPresentations
的信息,或者通过浏览 代码文档 。
EpoxyLayoutGroups
LayoutGroups 是受 SwiftUI 的 Auto Layout 容器启发,类似于 HStack
和 VStack
,允许您轻松地将 UIKit 元素组合成水平和垂直分组。
VGroup
可以将组件垂直组合在一起,以创建如下的堆叠组件。
源 结果
// Set of dataIDs to have consistent
// and unique IDs
enum DataID {
case title
case subtitle
case action
}
// Groups are created declaratively
// just like Epoxy ItemModels
let group = VGroup (
alignment : .leading ,
spacing : 8 )
{
Label.groupItem (
dataID : DataID.title ,
content : " Title text" ,
style : .title )
Label.groupItem (
dataID : DataID.subtitle ,
content : " Subtitle text" ,
style : .subtitle )
Button.groupItem (
dataID : DataID.action ,
content : " Perform action" ,
behaviors : .init { button in
print (" Button tapped! \( button ) " )
},
style : .standard )
}
// install your group in a view
group.install (in : view)
// constrain the group like you
// would a normal subview
group.constrainToMargins ()
如您所见,这与 Epoxy 中使用的其他 API 极为相似。需要注意的一个重要事项是在底部调用的 install(in: view)
。HGroup
和 VGroup
都使用 UILayoutGuide
编写,这防止了拥有大型嵌套视图层次结构。为了解决这个问题,我们添加了这个 install
方法,以免用户需要手动添加子视图和布局指南。
使用 HGroup
几乎与 VGroup
完全相同,但现在组件是横向布局而不是纵向布局。
源 结果
enum DataID {
case icon
case title
}
let group = HGroup (spacing : 8 ) {
ImageView.groupItem (
dataID : DataID.icon ,
content : UIImage (systemName : " person.fill" )! ,
style : .init (size : .init (width : 24 , height : 24 )))
Label.groupItem (
dataID : DataID.title ,
content : " This is an IconRow" )
}
group.install (in : view)
group.constrainToMargins ()
分组也支持嵌套,因此您可以使用多个分组轻松创建复杂布局。
源 结果
enum DataID {
case checkbox
case titleSubtitleGroup
case title
case subtitle
}
HGroup (spacing : 8 ) {
Checkbox.groupItem (
dataID : DataID.checkbox ,
content : .init (isChecked : true ),
style : .standard )
VGroupItem (
dataID : DataID.titleSubtitleGroup ,
style : .init (spacing : 4 ))
{
Label.groupItem (
dataID : DataID.title ,
content : " Build iOS App" ,
style : .title )
Label.groupItem (
dataID : DataID.subtitle ,
content : " Use EpoxyLayoutGroups" ,
style : .subtitle )
}
}
您可以在其 维基条目 中了解更多关于 EpoxyLayoutGroups
的信息,或者通过浏览 代码文档 。
常见问题解答(FAQ)
贡献
欢迎提交pull请求!我们期待您帮助我们改进这个库。请随意浏览开放的问题 ,寻找需要工作的事情。如果您有一个功能请求或错误,请打开一个新的问题,以便我们可以跟踪它。预期贡献者会遵守行为准则 。
许可
Epoxy是在Apache License 2.0下发布的。有关详细信息,请参阅代码许可 。
致谢
标志设计由Alana Hanada 和Jonard La Rosa 完成