Contents

airbnb/epoxy-ios

Epoxy is a suite of declarative UI APIs for building UIKit applications in Swift

Table of contents

CocoaPods Swift Package Manager (SPM)

EpoxyCollectionView EpoxyBars EpoxyNavigationController EpoxyPresentations * EpoxyLayoutGroups

Installation

Epoxy can be installed using CocoaPods or Swift Package Manager).

CocoaPods

To get started with Epoxy using Cocoapods add the following to your Podfile and then follow the integration instructions.

pod 'Epoxy'

Epoxy is separated into podspecs for each module so you only have to include what you need.

Swift Package Manager (SPM)

To install Epoxy using Swift Package Manager you can follow the tutorial published by Apple using the URL for the Epoxy repo with the current version:

  1. In Xcode, select “File” → “Swift Packages” → “Add Package Dependency”
  2. Enter https://github.com/airbnb/epoxy-ios.git

Epoxy is separated library products for each module so you only have to include what you need.

Modules

Epoxy has a modular architecture so you only have to include what you need for your use case:

| Module | Description | | ------ | ----------- | | Epoxy | Includes all of the below modules in a single import statement | | EpoxyCollectionView | Declarative API for driving the content of a UICollectionView | | EpoxyNavigationController | Declarative API for driving the navigation stack of a UINavigationController | | EpoxyPresentations | Declarative API for driving the modal presentations of a UIViewController | | EpoxyBars | Declarative API for adding fixed top/bottom bar stacks to a UIViewController | | EpoxyLayoutGroups | Declarative API for building composable layouts in UIKit with a syntax similar to SwiftUI's stack APIs | | EpoxyCore | Foundational APIs that are used to build all Epoxy declarative UI APIs |

Documentation and tutorials

For full documentation and step-by-step tutorials please check the wiki. For type-level documentation, see the Epoxy DocC documentation hosted on the Swift Package Index.

There's also a full sample app with a lot of examples that you can either run via the EpoxyExample scheme in Epoxy.xcworkspace or browse its source.

If you still have questions, feel free to create a new issue.

Getting started

### EpoxyCollectionView

`EpoxyCollectionView` provides a declarative API for driving the content of a `UICollectionView`. `CollectionViewController` is a subclassable `UIViewController` that lets you easily spin up a `UICollectionView`-backed view controller with a declarative API.

The following code samples will render a single cell in a `UICollectionView` with a `TextRow` component rendered in that cell. `TextRow` is a simple `UIView` containing two labels that conforms to the [`EpoxyableView`](https://github.com/airbnb/epoxy-ios/wiki/EpoxyCore#views) protocol.

You can either instantiate a `CollectionViewController` instance directly with sections, e.g. this view controller with a selectable row:

<table>
<tr><td> Source </td> <td> Result </td></tr>
<tr>
<td>

```swift
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
      }
  })
```

</td>
<td>

<img width="250" alt="Screenshot" src="docs/images/tap_me_example.png">

</td>
</tr>
</table>

Or you can subclass `CollectionViewController` for more advanced scenarios, e.g. this view controller that keeps track of a running count:

<table>
<tr><td> Source </td> <td> Result </td></tr>
<tr>
<td>

```swift
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
      }
  }
}
```
</td>
<td>

<img width="250" alt="Screenshot" src="docs/images/counter_example.gif">

</td>
</tr>
</table>

You can learn more about `EpoxyCollectionView` in its [wiki entry](https://github.com/airbnb/epoxy-ios/wiki/EpoxyCollectionView), or by browsing the [code documentation](https://swiftpackageindex.com/airbnb/epoxy-ios/master/documentation/epoxycollectionview).

### EpoxyBars

`EpoxyBars` provides a declarative API for rendering fixed top, fixed bottom, or [input accessory](https://developer.apple.com/documentation/uikit/uiresponder/1621119-inputaccessoryview) bar stacks in a `UIViewController`.

The following code example will render a `ButtonRow` component fixed to the bottom of the `UIViewController`'s view. `ButtonRow` is a simple `UIView` component that contains a single `UIButton` constrained to the margins of the superview that conforms to the [`EpoxyableView`](https://github.com/airbnb/epoxy-ios/wiki/EpoxyCore#views) protocol:

<table>
<tr><td> Source </td> <td> Result </td></tr>
<tr>
<td>

```swift
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
      }))
  }
}
```

</td>
<td>

<img width="250" alt="Screenshot" src="docs/images/bottom_button_example.png">

</td>
</tr>
</table>

You can learn more about `EpoxyBars` in its [wiki entry](https://github.com/airbnb/epoxy-ios/wiki/EpoxyBars), or by browsing the [code documentation](https://swiftpackageindex.com/airbnb/epoxy-ios/master/documentation/epoxybars).

### EpoxyNavigationController

`EpoxyNavigationController` provides a declarative API for driving the navigation stack of a `UINavigationController`.

The following code example shows how you can use this to easily drive a feature that has a flow of multiple view controllers:

<table>
<tr><td> Source </td> <td> Result </td></tr>
<tr>
<td>

```swift
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
        })
    }
  }
}
```

</td>
<td>

<img width="250" alt="Screenshot" src="docs/images/form_navigation_example.gif">

</td>
</tr>
</table>

You can learn more about `EpoxyNavigationController` in its [wiki entry](https://github.com/airbnb/epoxy-ios/wiki/EpoxyNavigationController), or by browsing the [code documentation](https://swiftpackageindex.com/airbnb/epoxy-ios/master/documentation/epoxynavigationcontroller).

### EpoxyPresentations

`EpoxyPresentations` provides a declarative API for driving the modal presentation of a `UIViewController`.

The following code example shows how you can use this to easily drive a feature that shows a modal when it first appears:

<table>
<tr><td> Source </td> <td> Result </td></tr>
<tr>
<td>

```swift
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
        })
    }
  }
}
```

</td>
<td>

<img width="250" alt="Screenshot" src="docs/images/modal_example.gif">

</td>
</tr>
</table>

You can learn more about `EpoxyPresentations` in its [wiki entry](https://github.com/airbnb/epoxy-ios/wiki/EpoxyPresentations), or by browsing the [code documentation](https://swiftpackageindex.com/airbnb/epoxy-ios/master/documentation/epoxypresentations).

EpoxyLayoutGroups

LayoutGroups are UIKit Auto Layout containers inspired by SwiftUI's HStack and VStack that allow you to easily compose UIKit elements into horizontal and vertical groups.

VGroup allows you to group components together vertically to create stacked components like this:

<table> <tr><td> Source </td> <td> Result </td></tr> <tr> <td>

// 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()

</td> <td>

<img alt="ActionRow screenshot" src="docs/images/ActionRow.png">

</td> </tr> </table>

As you can see, this is incredibly similar to the other APIs used in Epoxy. One important thing to note is that install(in: view) call at the bottom. Both HGroup and VGroup are written using UILayoutGuide which prevents having large nested view hierarchies. To account for this, we’ve added this install method to prevent the user from having to add subviews and the layout guide manually.

Using HGroup is almost exactly the same as VGroup but the components are now horizontally laid out instead of vertically:

<table> <tr><td> Source </td> <td> Result </td></tr> <tr> <td>

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()

</td> <td>

<img alt="IconRow screenshot" src="docs/images/IconRow.png">

</td> </tr> </table>

Groups support nesting too, so you can easily create complex layouts with multiple groups:

<table> <tr><td> Source </td> <td> Result </td></tr> <tr> <td>

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)
  }
}

</td> <td>

<img alt="IconRow screenshot" src="docs/images/CheckboxRow.png">

</td> </tr> </table>

You can learn more about EpoxyLayoutGroups in its wiki entry, or by browsing the code documentation.

FAQ

Contributing

Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it. Contributors are expected to follow the Code of Conduct.

License

Epoxy is released under the Apache License 2.0. See LICENSE for details.

Credits

Logo design by Alana Hanada and Jonard La Rosa

Package Metadata

Repository: airbnb/epoxy-ios

Stars: 1313

Forks: 76

Open issues: 9

Default branch: master

Primary language: swift

License: Apache-2.0

Topics: ios, swift, uicollectionview, uikit, uinavigationcontroller

README: README.md