EpoxyNavigationController 0.9.0

EpoxyNavigationController 0.9.0

由以下人员维护:Eric HoracekTyler HedrickKieraj Mumick



  • 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 Packages”→“添加包依赖”
  2. 输入 https://github.com/airbnb/epoxy-ios.git

Epoxy 为每个 模块 分割成 库产品,您只需包含所需的即可。

模块

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

模块 说明
Epoxy 以下所有模块都可以通过单个导入语句包括在内
EpoxyCollectionView Epoxy 的声明式 API,用于驱动 UICollectionView 的内容
EpoxyNavigationController Epoxy 的声明式 API,用于驱动 UINavigationController 的导航堆栈
EpoxyPresentations Epoxy 的声明式 API,用于驱动 UIViewController 的模态展示
EpoxyBars Epoxy 的声明式 API,用于向 UIViewController 添加固定顶部/底部工具栏
EpoxyLayoutGroups Epoxy 的声明式 API,用于在 UIKit 中使用类似 SwiftUI's stack APIs 的语法构建可组合的布局
EpoxyCore 构建所有 Epoxy 声明式 UI API 的基础 API

文档和教程

有关完整文档和分步教程,请参阅 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
      }
  })
Screenshot

或者,您可以对该视图控制器进行子类化以达到更高级的情况,例如以下跟踪运行计数的视图控制器

结果
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 视图底部的 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
      }))
  }
}
Screenshot

您可以在其 维基条目 或通过浏览 代码文档 来了解有关 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
        })
    }
  }
}
Screenshot

您可以在其 维基条目 中了解更多关于 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
        })
    }
  }
}
Screenshot

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

EpoxyLayoutGroups

LayoutGroups 是受 SwiftUI 的 Auto Layout 容器启发,类似于 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

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

常见问题解答(FAQ)

贡献

欢迎提交pull请求!我们期待您帮助我们改进这个库。请随意浏览开放的问题,寻找需要工作的事情。如果您有一个功能请求或错误,请打开一个新的问题,以便我们可以跟踪它。预期贡献者会遵守行为准则

许可

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

致谢

标志设计由Alana HanadaJonard La Rosa完成