EpoxyBars 0.9.0

EpoxyBars 0.9.0

Eric HoracekKieraj MumickTyler Hedrick 维护。



EpoxyBars 0.9.0

  • Airbnb 提供

Epoxy logo

Build Status Swift Package Manager compatible Platform Swift Versions

Epoxy 是一套用于在 Swift 中构建 UIKit 应用程序的声明性 UI API。Epoxy 受 Android 中杰出的 Epoxy 框架 以及其他 Swift 中的声明性 UI 框架(如 SwiftUI)的启发。

Epoxy 是在 Airbnb 开发的,并为数百万用户的产品中的应用程序中的数千个屏幕提供动力。它已经经过多年由 数十名贡献者 开发和精炼。

以下是使用 Epoxy 构建的 Airbnb 应用程序的一些样本屏幕。我们对 Epoxy 的使用从最简单的表单和静态屏幕到最复杂和动态的特性。

主页详情 主页照片 信息发送 注册
Home Details Home Photos Messaging Registration

目录

安装

Epoxy 可以通过 CocoaPodsSwift 包管理器 进行安装。

CocoaPods

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

pod 'Epoxy'

Epoxy 分别为每个 podspec 取名,以保证您只需包含所需的模块。

Swift Package Manager (SPM)

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

  1. 在 Xcode 中,选择“文件”→“Swift 包”→“添加包依赖项”。
  2. 输入 https://github.com/airbnb/epoxy-ios.git

Epoxy 将每个 模块 分别分成库产品,因此您只需要包含所需的模块。

模块

Epoxy 具有模块化结构,因此您只需包含针对您的用例所需的模块。

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

文档和教程

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

此外,还有一个包含许多示例的全功能示例应用程序,您可以通过 Epoxy.xcworkspace 中的 EpoxyExample 方案运行它,或者浏览它的 源代码

如果您还有疑问,请随时创建一个新的问题

入门指南

EpoxyCollectionView

EpoxyCollectionView 提供了一个声明式 API 来驱动 UICollectionView 的内容。 CollectionViewController 是一个可子类化的 UIViewController,它允许您轻松地使用声明式 API 初始化一个基于 UICollectionView 的 view controller。

下面的代码示例将渲染一个带有 TextRow 组件的单元格,该组件在该单元格中渲染。 TextRow 是一个简单的 UIView,包含两个标签,符合 EpoxyableView 协议。

您可以直接使用带区段的 CollectionViewController 实例,例如此具有可选项的无序列表 view controller

源代码 结果
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,例如此跟踪运行计数的 view controller

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

您可以在其 维基条目 中了解有关 EpoxyCollectionView 的更多信息,或者通过浏览 代码文档

EpoxyBars

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

以下代码示例将渲染一个固定位于 UIViewController 视图底部的 ButtonRow 组件。 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
      }))
  }
}
Screenshot

您可以在其 维基条目 中了解有关 EpoxyBars 的更多信息,或者通过浏览 代码文档

EpoxyNavigationController

EpoxyNavigationController 为驱动 UINavigationController 的导航堆栈提供了声明性 API。

以下代码示例展示了如何轻松驱动具有多个视图控制器流动的特征。

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

您可以在其 维基页面 或通过浏览 代码文档 了解更多关于 EpoxyNavigationController 的信息。

EpoxyPresentations

EpoxyPresentations 为驱动 UIViewController 的模态显示提供了声明性 API。

以下代码示例展示了如何在使用时轻松地驱动首次出现时显示模态的特征。

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

您可以在其 维基页面 或通过浏览 代码文档 了解更多关于 EpoxyPresentations 的信息。

EpoxyLayoutGroups

布局组是受 SwiftUI 的 自动布局 启发,类似 HStackVStack 的 UIKit 容器,允许您轻松将 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

您可以在其维基页面或通过浏览代码文档了解更多有关 EpoxyLayoutGroups 的信息。

常见问题解答 (FAQ)

贡献

欢迎提出 pull request!我们很乐意得到帮助,改善这个库。请随意查看开放的 问题 并寻找需要改进的地方。如果有功能请求或缺陷,请开一个新的 issue,我们可以跟踪它。贡献者应遵守行为准则

许可证

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

鸣谢

标志设计由 Alana HanadaJonard La Rosa 完成