。
为在 UITableView 和 UICollectionView 中构建基于组件的用户界面而生的声明性库声明式 | 基于组件的 | 非破坏性 |
---|---|---|
提供了带有 diff 算法的强大功能的声明式设计,以构建列表 UI。 | 声明一次组件,它可以在任何类型的列表元素中重用。 | 不破坏 UIKit,通过架构和算法解决了各种问题。 |
简介
Carbon 是一个响应 SwiftUI 和 React 的启发,用于构建基于组件的用户界面库在 UITableView 和 UICollectionView 上。
这使构建和维护复杂 UI 变得容易。
由于用 Carbon
创建的组件可以直接在 SwiftUI
上工作,因此未来迁移的成本可以大大降低。
使用了基于保罗·赫克尔论文高度优化的 DifferenceKit。
声明式设计和 diff 算法使您的代码更可预测,调试更容易,并为用户提供美丽的动画。
我们的目标与 Instagram/IGListKit 和 airbnb/Epoxy 类似,我们将这些库视为先驱。
例子
![]() |
![]() |
![]() |
![]() |
![]() |
---|
renderer.render {
Header("GREET")
.identified(by: \.title)
HelloMessage("Vincent")
HelloMessage("Jules")
HelloMessage("Mia")
Footer("👋 Greeting from Carbon")
.identified(by: \.text)
}
SwiftUI 兼容性
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("GREET")
.font(.title)
.padding(.horizontal, 16)
HelloMessage("World")
.frame(height: 60)
.background(Color.red)
}
}
}
}
入门指引
开发构建
$ git clone https://github.com/ra1028/Carbon.git
$ cd Carbon/
$ make setup
$ open Carbon.xcworkspace
基本用法
以下是使用 Carbon 构建 UI 列表的基础知识。
API 文档 将帮助您了解每个类型的详细信息。
有关更高级的使用方法,请参阅 高级指南。
更实际的示例在 这里。
组件
Component
是 Carbon 中 UI 的基本单元。
所有元素都由组件组成,并且可以通过更新差异进行动画。
UIView
,UIViewController
及其子类可以通过默认的方式作为组件的 content
使用。
您可以通过实现 referenceSize(in bounds: CGRect) -> CGSize?
声明固定大小的组件。默认返回 nil,并回退到如 UITableView.rowHeight
或 UICollectionViewFlowLayout.itemSize
等值。
有关组件的更多信息,请参阅 此处。
以下是最简单的实现方式。
struct HelloMessage: Component {
var name: String
func renderContent() -> UILabel {
UILabel()
}
func render(in content: UILabel) {
content.text = "Hello \(name)"
}
}
用作单元格的组件需要指定一个任意的 id
。
通过 Component.identified(by:)
或使用 IdentifiableComponent
协议来指定一个 id
。
渲染器
组件通过 Renderer.render
在列表 UI 中显示。
在 Carbon 中不再需要注册元素类型到表格视图等模板。
适配器充当代理和数据源,更新器处理更新。
您还可以通过继承和定制来更改行为。
还有 UITableViewReloadDataUpdater
和 UICollectionViewReloadDataUpdater
,它们通过 reloadData
进行更新,不进行差异检测。
当再次调用 render
时,更新器会从当前渲染的组件中计算差异,并通过系统动画进行更新。
UITableView 的渲染器
@IBOutlet var tableView: UITableView!
let renderer = Renderer(
adapter: UITableViewAdapter(),
updater: UITableViewUpdater()
)
override func viewDidLoad() {
super.viewDidLoad()
renderer.target = tableView
}
UICollectionView 的渲染器
@IBOutlet var collectionView: UICollectionView!
let renderer = Renderer(
adapter: UICollectionViewFlowLayoutAdapter(),
updater: UICollectionViewUpdater()
)
override func viewDidLoad() {
super.viewDidLoad()
renderer.target = collectionView
}
渲染组件
renderer.render {
Header("GREET")
.identified(by: \.title)
HelloMessage("Butch")
HelloMessage("Fabianne")
}
区域
一个区域可以包括标题、页脚和单元格。
这还需要在多个区域中指定 id
以进行识别。
可以使用以下函数构造器声明单元格:
let appearsBottomSection: Bool = ...
let appearsFourthMan: Bool = ...
renderer.render {
Section(
id: "Bottom",
header: Header("GREET"),
footer: Footer("👋 Greeting from Carbon"),
cells: {
HelloMessage("Marsellus")
HelloMessage("The Wolf")
}
)
if appearsBottomSection {
Section(id: "Top") {
HelloMessage("Brett")
HelloMessage("Roger")
if appearsFourthMan {
HelloMessage("Fourth Man")
}
}
}
}
分组
使用函数构造器语法声明单元格或区域的数量限制为 10
。您可以通过使用 Group
来避免这个限制。
它还可以用于从包含 N 个元素的数组中创建单元格或区域。
组件分组
renderer.render {
Group {
Header("GREET")
.identified(by: \.title)
HelloMessage("Vincent")
HelloMessage("Jules")
}
Group(of: ["Pumpkin", "Honey Bunny"]) { name in
HelloMessage(name)
}
}
区域分组
renderer.render {
Group {
Section(id: 0) {
HelloMessage("Jimmie")
}
Section(id: 1) {
HelloMessage("Bonnie")
}
}
Group(of: ["Lance", "Jody"]) { name in
Section(id: name) {
HelloMessage(name)
}
}
}
[了解更多使用情况] [查看示例应用]
高级指南
自定义内容
当然,组件内容可以使用自定义类。您也可以从 Xib 中实例化它。
它可以继承任何类,但最常见的方法是继承 UIView
或 UIViewController
。
class HelloMessageContent: UIView {
@IBOutlet var label: UILabel!
}
struct HelloMessage: Component {
var name: String
func renderContent() -> HelloMessageContent {
HelloMessageContent.loadFromNib() // Extension for instantiate from Xib. Not in Carbon.
}
func render(in content: HelloMessageContent) {
content.label.text = "Hello \(name)"
}
}
可识别组件
IdentifiableComponent
是一个可以预定义标识符的组件。
如果组件符合 Hashable
,则可以省略 id
的定义。
struct HelloMessage: IdentifiableComponent {
var name: String
var id: String {
name
}
...
SwiftUI 兼容性
使用 Carbon
制作的组件与 SwiftUI
兼容。
通过组合 View
协议,组件可以轻松地作为 SwiftUI 使用。
目前 SwiftUI 不支持自定尺寸,因此可以使用 UIView.intrinsicContentSize
或通过 Component.referenceSize(in:)
或 View.frame(height:)
显式指定高度。
struct HelloMessage: Component, View {
...
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("GREET")
HelloMessage("World")
.frame(height: 60)
Spacer()
}
}
}
深入组件
组件可以定义更详细的行为。
以下是一些示例。
-
shouldContentUpdate(with next: Self) -> Bool
如果结果为true
,则组件作为单元格显示时将单独重新加载,段落或页脚将与整个段落一起重新加载。
默认返回false
,但更新器将始终重新渲染可见组件的变化。 -
referenceSize(in bounds: CGRect) -> CGSize?
定义列表 UI 中组件的大小。
可以通过返回nil
使用默认值,例如UITableView.rowHeight
或UICollectionViewLayout.itemSize
。
默认返回nil
。 -
shouldRender(next: Self, in content: Content) -> Bool
通过返回false
,可以在重新加载或提取元素时跳过组件的重新渲染。
与下一个值比较而非重新渲染时,检测组件的变化。
这建议仅用于性能调整。 -
contentWillDisplay(_ content: Content)
每当组件进入可见区域前都会调用。 -
contentDidEndDisplay(_ content: Content)
每当组件从可见区域退出后都会调用。
选择
可以通过将 didSelect
设置为 UITableViewAdapter
或
renderer.adapter.didSelect { context in
print(context)
}
但是,我们建议将组件的 Content
设置为继承自 UIControl
的类。
这更有利于维护和扩展。
class MenuItemContent: UIControl {
@IBOutlet var label: UILabel!
var onSelect: (() -> Void)?
@objc func handleSelect() {
onSelect?()
}
override func awakeFromNib() {
super.awakeFromNib()
addTarget(self, action: #selector(handleSelect), for: .touchUpInside)
}
}
struct MenuItem: Component {
var text: String
var onSelect: () -> Void
func renderContent() -> MenuItemContent {
MenuItemContent.loadFromNib()
}
func render(in content: MenuItemContent) {
content.label.text = text
content.onSelect = onSelect
}
}
通过这种方式,为了取消滚动时的选择,需要实现以下扩展。
extension UITableView {
open override func touchesShouldCancel(in view: UIView) -> Bool {
true
}
}
extension UICollectionView {
open override func touchesShouldCancel(in view: UIView) -> Bool {
true
}
}
适配器定制
通过子类化每个适配器,可以添加 delegate
和 dataSource
方法。
class CustomTableViewdapter: UITableViewAdapter {
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
"Header title for section\(section)"
}
}
let renderer = Renderer(
adapter: CustomTableViewAdapter(),
updater: UITableViewUpdater()
)
此外,还可以通过在适配器中重写一些方法来自定义(cell、header、footer)等元素的类作为组件的容器。
也可以通过传入 nib 作为返回值 Registration 的初始化参数来使用 xib
。
class CustomTableViewAdapter: UITableViewAdapter {
// Use custom cell.
override func cellRegistration(tableView: UITableView, indexPath: IndexPath, node: CellNode) -> CellRegistration {
CellRegistration(class: CustomTableViewCell.self)
}
// Use custom header view.
override func headerViewRegistration(tableView: UITableView, section: Int, node: ViewNode) -> ViewRegistration {
ViewRegistration(class: CustomTableViewHeaderFooterView.self)
}
// Use custom footer view.
override func footerViewRegistration(tableView: UITableView, section: Int, node: ViewNode) -> ViewRegistration {
ViewRegistration(class: CustomTableViewHeaderFooterView.self)
}
}
在
class CustomCollectionViewAdapter: UICollectionViewAdapter {
override func supplementaryViewNode(forElementKind kind: String, collectionView: UICollectionView, at indexPath: IndexPath) -> ViewNode? {
switch kind {
case "CustomSupplementaryViewKindSectionHeader":
return headerNode(in: indexPath.section)
default:
return super.supplementaryViewNode(forElementKind: kind, collectionView: collectionView, at: indexPath)
}
}
}
更新器定制
通过继承 Updater
可以修改列表 UI 的更新行为。
这是让 Carbon 适应您项目的关键。
以下是 updater
提供的一些默认设置。
-
isAnimationEnabled
表示是否对差异更新的动画进行启用,设置false
将使用UIView.performWithoutAnimation
进行操作。
默认值为true
。 -
isAnimationEnabledWhileScrolling
表示是否在目标滚动时对差异更新的动画进行启用,设置false
将使用UIView.performWithoutAnimation
进行操作。
默认值为false
。 -
animatableChangeCount
执行差异更新的最大更改数。如果超过此数,将回退到reloadData
。
默认值为300
。 -
keepsContentOffset
表示是否在更新后重置内容偏移。
在某些情况下,差异更新后内容偏移可能变成意外位置。如果设置为true
,则更新后将还原内容偏移。
默认值为true
。
无 FunctionBuilder 语法
碳也可以使用声明式语法构建UI,而不需要使用功能构建器,如下所示。
- ViewNode
这是一个表示头部或脚部的节点。该节点封装了符合Component
协议的实例。
ViewNode(Header("GREET"))
- CellNode
CellNode
是一个表示单元格的节点。
与ViewNode不同,这需要一个id
,它是一个可以识别许多单元格中的某个单元格的Hashable
类型的标识符。
该id
用于在更改前后找到列表数据中相同的组件。
CellNode(id: 0, HelloMessage("Jules"))
CellNode(HelloMessage("Jules").identified(by: \.name))
CellNode(HelloMessage("Jules")) // Using `IdentifiableComponent`.
- 分区和渲染
renderer.render(
Section(
id: "Section",
header: ViewNode(Header("GREET")),
cells: [
CellNode(HelloMessage("Vincent")),
CellNode(HelloMessage("Mia")),
CellNode(HelloMessage("Jules"))
],
footer: ViewNode(Footer("👋 Greeting from Carbon"))
)
)
需求
- Swift 5.1+
- Xcode 11.0+
安装
CocoaPods
在你的Podfile
中添加以下内容
pod 'Carbon'
Carthage
在你的Cartfile
中添加以下内容
github "ra1028/Carbon"
Swift Package Manager
在Xcode菜单中选择文件 > Swift包 > 添加包依赖...
并通过GUI输入仓库URL。
Repository: https://github.com/ra1028/Carbon
贡献
欢迎提交拉取请求、错误报告和功能请求
请参阅 贡献指南文件,了解如何为Carbon做出贡献。
尊重
我真诚地尊重使用差异算法的列表UI库。
- React (Facebook开发)
我非常受其范式和API设计的启发。 - IGListKit (Instagram开发)
目前在iOS中,列表UI库中应用差异算法最流行的库。 - Epoxy (Airbnb开发)
在Android列表UI库中,应用差异算法最流行的库。 - RxDataSources (@kzaher和RxSwift Community开发)
一个出色的库,可以非常快速地通过算法实现复杂的差异更新。 - Texture (TextureGroup、Facebook、Pinterest开发)
创建列表UI的唯一库,追求渲染性能。 - Bento (Babylon Health开发)
Bento是一个功能强大的声明性库,其API设计遵循React。 - ReactiveLists (PlanGrid开发)
使用DifferenceKit以及Carbon进行差异算法。
许可
Carbon是在Apache 2.0许可下发布的。