Epoxy logo

Build Status Swift Package Manager compatible Platform Swift Versions

Epoxy 是一套声明式 UI API,用于在 Swift 中构建 UIKit 应用程序。 Epoxy 的灵感来自优秀的 Android 上的 Epoxy 框架,以及 Swift 中其他声明式 UI 框架,例如 SwiftUI

Epoxy 在 Airbnb 开发,并驱动着数百万用户使用的应用程序中的数千个屏幕。 它经过了 数十位贡献者 多年的开发和完善。

以下是 Airbnb 应用程序中使用 Epoxy 构建的一些示例屏幕。 我们对 Epoxy 的使用范围从最简单的表单和静态屏幕到最先进和动态的功能。

房源详情 房源照片 消息 注册
Home Details Home Photos Messaging Registration

目录

安装

可以使用 CocoaPodsSwift Package Manager 安装 Epoxy。

CocoaPods

要开始使用 Epoxy,请使用 Cocoapods 将以下内容添加到您的 Podfile,然后按照集成说明操作。

pod 'Epoxy'

Epoxy 被分成每个 模块podspecs,因此您只需包含您需要的内容。

Swift Package Manager (SPM)

要使用 Swift Package Manager 安装 Epoxy,您可以按照 Apple 发布的教程,使用 Epoxy 仓库的 URL 和当前版本

  1. 在 Xcode 中,选择“File”→“Swift Packages”→“Add Package Dependency”
  2. 输入 https://github.com/airbnb/epoxy-ios.git

Epoxy 被分成每个 模块库产品,因此您只需包含您需要的内容。

模块

Epoxy 具有模块化架构,因此您只需包含您用例所需的内容

模块 描述
Epoxy 在单个导入语句中包含以下所有模块
EpoxyCollectionView 用于驱动 UICollectionView 内容的声明式 API
EpoxyNavigationController 用于驱动 UINavigationController 导航堆栈的声明式 API
EpoxyPresentations 用于驱动 UIViewController 模态呈现的声明式 API
EpoxyBars 用于向 UIViewController 添加固定的顶部/底部栏堆栈的声明式 API
EpoxyLayoutGroups 用于在 UIKit 中构建可组合布局的声明式 API,其语法类似于 SwiftUI 的堆栈 API
EpoxyCore 用于构建所有 Epoxy 声明式 UI API 的基础 API

文档和教程

有关完整的文档和分步教程,请查看 wiki。 有关类型级别的文档,请参阅 托管在 Swift Package Index 上的 Epoxy DocC 文档。

还有一个包含大量示例的完整示例应用程序,您可以运行 Epoxy.xcworkspace 中的 EpoxyExample 方案来运行它,或者浏览其 源代码

如果您仍然有疑问,请随时创建新的 issue

开始使用

EpoxyCollectionView

EpoxyCollectionView 提供了一个声明式 API,用于驱动 UICollectionView 的内容。 CollectionViewController 是一个可子类化的 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
      }
  })
Screenshot

或者您可以子类化 CollectionViewController 以获得更高级的场景,例如,这个视图控制器可以跟踪正在运行的计数

源代码 结果
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
      }
  }
}
Screenshot

您可以在其wiki 条目或浏览 代码文档中了解有关 EpoxyCollectionView 的更多信息。

EpoxyBars

EpoxyBars 提供了一个声明式 API,用于在 UIViewController 中渲染固定的顶部、固定的底部或 输入配件 栏堆栈。

以下代码示例将渲染一个固定到 UIViewController 视图底部的 ButtonRow 组件。 ButtonRow 是一个简单的 UIView 组件,其中包含一个约束到符合 EpoxyableView 协议的 superview 边距的单个 UIButton

源代码 结果
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
      }))
  }
}
Screenshot

您可以在其wiki 条目或浏览 代码文档中了解有关 EpoxyBars 的更多信息。

EpoxyNavigationController

EpoxyNavigationController 提供了一个声明式 API,用于驱动 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
        })
    }
  }
}
Screenshot

您可以在其wiki 条目或浏览 代码文档中了解有关 EpoxyNavigationController 的更多信息。

EpoxyPresentations

EpoxyPresentations 提供了一个声明式 API,用于驱动 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
        })
    }
  }
}
Screenshot

您可以在其wiki 条目或浏览 代码文档中了解有关 EpoxyPresentations 的更多信息。

EpoxyLayoutGroups

LayoutGroups 是 UIKit Auto Layout 容器,其灵感来自 SwiftUI 的 HStackVStack,允许您轻松地将 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()
ActionRow screenshot

如您所见,这与 Epoxy 中使用的其他 API 非常相似。需要注意的一点是底部的 install(in: view) 调用。 HGroupVGroup 都是使用 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()
IconRow screenshot

组也支持嵌套,因此您可以轻松地使用多个组创建复杂的布局

源代码 结果
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)
  }
}
IconRow screenshot

您可以在其wiki 条目或浏览 代码文档中了解有关 EpoxyLayoutGroups 的更多信息。

常见问题解答

贡献

欢迎提交 Pull Request! 我们很乐意帮助改进这个库。 随意浏览开放的 issues 以查找需要完成的工作。 如果您有功能请求或错误,请打开一个新 issue,以便我们可以跟踪它。 贡献者应遵守行为准则

许可证

Epoxy 在 Apache License 2.0 下发布。 有关详细信息,请参阅 LICENSE

致谢

徽标设计者:Alana HanadaJonard La Rosa