Carbon 1.0.0-rc.6

Carbon 1.0.0-rc.6

ra1028 维护。



Carbon 1.0.0-rc.6

  • ra1028

为在 UITableView 和 UICollectionView 中构建基于组件的用户界面而生的声明性库

声明式 基于组件的 非破坏性
提供了带有 diff 算法的强大功能的声明式设计,以构建列表 UI。 声明一次组件,它可以在任何类型的列表元素中重用。 不破坏 UIKit,通过架构和算法解决了各种问题。

Release CocoaPods Carthage
CI Status Swift 5.1 Platform Lincense


简介

Carbon 是一个响应 SwiftUIReact 的启发,用于构建基于组件的用户界面库在 UITableView 和 UICollectionView 上。
这使构建和维护复杂 UI 变得容易。
由于用 Carbon 创建的组件可以直接在 SwiftUI 上工作,因此未来迁移的成本可以大大降低。

使用了基于保罗·赫克尔论文高度优化的 DifferenceKit
声明式设计和 diff 算法使您的代码更可预测,调试更容易,并为用户提供美丽的动画。

我们的目标与 Instagram/IGListKitairbnb/Epoxy 类似,我们将这些库视为先驱。


例子

Pangram Kyoto Emoji Todo Form

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 的基本单元。
所有元素都由组件组成,并且可以通过更新差异进行动画。

UIViewUIViewController 及其子类可以通过默认的方式作为组件的 content 使用。
您可以通过实现 referenceSize(in bounds: CGRect) -> CGSize? 声明固定大小的组件。默认返回 nil,并回退到如 UITableView.rowHeightUICollectionViewFlowLayout.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 中不再需要注册元素类型到表格视图等模板。

适配器充当代理和数据源,更新器处理更新。
您还可以通过继承和定制来更改行为。
还有 UITableViewReloadDataUpdaterUICollectionViewReloadDataUpdater,它们通过 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 中实例化它。
它可以继承任何类,但最常见的方法是继承 UIViewUIViewController

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.rowHeightUICollectionViewLayout.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
    }
}

适配器定制

通过子类化每个适配器,可以添加 delegatedataSource 方法。

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许可下发布的。


Carbon Logo