Git Product home page Git Product logo

epoxy-ios's Introduction

Epoxy logo

Build Status Swift Package Manager compatible Platform Swift Versions

Epoxy is a suite of declarative UI APIs for building UIKit applications in Swift. Epoxy is inspired and influenced by the wonderful Epoxy framework on Android, as well as other declarative UI frameworks in Swift such as SwiftUI.

Epoxy was developed at Airbnb and powers thousands of screens in apps that are shipped to millions of users. It has been developed and refined for years by dozens of contributors.

Below are a few sample screens from the Airbnb app that we've built using Epoxy. Our usages of Epoxy span from our simplest forms and static screens to our most advanced and dynamic features.

Home Details Home Photos Messaging Registration
Home Details Home Photos Messaging Registration

Table of contents

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 protocol.

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

Source Result
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

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

Source Result
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

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

EpoxyBars

EpoxyBars provides a declarative API for rendering fixed top, fixed bottom, or input accessory 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 protocol:

Source Result
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

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

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:

Source Result
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

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

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:

Source Result
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

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

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:

Source Result
// 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

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:

Source Result
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

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

Source Result
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

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

epoxy-ios's People

Contributors

bryankeller avatar brynbodayle avatar bscazzero avatar calda avatar davidhexd avatar dcramps avatar dependabot[bot] avatar dfed avatar erichoracek avatar fdiaz avatar grnbeagle avatar jqsilver avatar julianozen avatar kierajmumick avatar lauraskelton avatar lepond avatar matthewcheok avatar miguel-jimenez-0529 avatar muhuaxin avatar nickffox avatar rafael-assis avatar rugoli avatar shepting avatar swiftal64 avatar teradyl avatar thedrick avatar yfrancis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

epoxy-ios's Issues

Release Bump?

Are there plans to make another release? Last one was back in Dec.

What is the equivalent of distribution for GroupItem (If you want fillEqual, equalSpacing, equalCentering)

I would like to create two labels that have the same width and a spacing between them. Like fillEqual in stackview.
Is there a way to do this?

The best idea I have come up with is this:

vgroup.setItems {
      vgroup.setItems {
      HGroupItem(dataID: DataID.group, style: .init()) {
        Label.groupItem(dataID: DataID.leading, content: .init(string: "Leading Label2"), style: .init())
          .backgroundColor(.green)
        VGroupItem(dataID: DataID.spacerGroup, style: .init(alignment: .centered(to: vgroup), spacing: 0)) {
          SpacerItem(dataID: DataID.spacer, style: .init(fixedWidth: 20))
        }
        Label.groupItem(dataID: DataID.trailing, content: .init(string: "Trailing Label"), style: .init())
          .backgroundColor(.red)
      }
    }

But then I get
Screenshot 2022-08-03 at 08 19 05
and this is what I want
Screenshot 2022-08-03 at 08 19 05

Using a different renderer besides UIKit

Given the instantiation cost of UIViews and delays in rendering, both of which must be done on the main thread, it'd be cool to support an alternate renderer. It could be one that's thread-safe, does less work, or both. Perhaps Flutter's Skia would be worth playing with, which I've heard anecdotally can be faster.

iOS 9 supporting issues

Hi, it seems epoxy-iOS require the target os version of iOS 13+, is it possible to run epoxy in legacy os like iOS9+?

SwiftUI views don't get reused properly

When using SwiftUI views in an Epoxy Collection View, views are reused and onAppear is called for each one correctly most of the time, but not if you scroll up and down really fast (causing very fast cell reuse). See video:

Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-09-29.at.17.19.22.mp4

Repro case:

import Epoxy
import SwiftUI
import UIKit

final class SwiftUIInEpoxyViewController: CollectionViewController {

  init() {
    super.init(layout: UICollectionViewCompositionalLayout.listNoDividers)
    setItems(items, animated: false)
  }

  private var items: [ItemModeling] {
    (1...100).map { (index: Int) in
      SwiftUIView(value: index).itemModel(dataID: index)
    }
  }
}

private final class Store: ObservableObject {
  @Published var value: Int?
}

private struct SwiftUIView: View {

  let value: Int

  @StateObject private var store = Store()

  var body: some View {
    HStack {
      Text("\(value)")
      if let storeValue = store.value {
        Spacer()
        Text("\(storeValue)").foregroundColor(storeValue == value ? .black : .red)
      }
    }
    .padding(.horizontal)
    .onAppear {
      store.value = value
    }
  }

}

It is recommended to make `Content` following `Equatable` optional.

Suppose I have a more complex EpoxyableView that has a Content that needs to be set using multiple values.

Considering the reusability of this view, I want to use an abstract protocol to represent the properties it needs. This would reduce the number of conversions between models and improve performance.

But Equatable got in the way when I tried to use a protocol to represent Content.

I did notice the following in the documentation:

The Content type being Equatable is used by EpoxyCollectionView to perform performant diffing when setting sections on a CollectionView.

I prefer to use a more comfortable way of writing my programs than the performance gains that Equatable brings. When I need more extreme performance, I will consider following the Equatable protocol, but do not want to add Equatable to every model.

So I'd like Content to stop requiring Equatable to be implemented.

Maybe using as? at the call would be a good way to go?

VGroup stretched items when given absolute size in UICollectionViewCompositionalLayout.

Hi all! Thanks for the awesome library :) very clean and easy to use.

I had one question when implementing a view using VGroup, so basically when given absolute size for NSCollectionLayoutItem, VGroup is not laying out things from top to bottom based on their intrinsicContentSize, instead it would stretch some items in the group to fill the entire view. Wondering is there anyway I can config this behaviour, so items are rendered from top to bottom based on their content size?

Thank you so much!

How can I add section decoration items using Epoxy?

First of all let me start this by saying thanks for finally open sourcing Epoxy for iOS.
I've started playing around with this since yesterday and so far I'm very happy with what it can do.

Now back to the question, I couldn't find anything in the wiki or in the code about decoration items. Is there a straight forward way to add decoration items to sections using Epoxy? And if it's not supported at the moment is there a plan to add this feature?

CollectionViewController doesn't abide by layoutMargins or top NSCollectionLayoutGroup.contentInsets

I'm trying to inset my content in my CollectionViewController subclass, and I'm not sure how to do it.

First I tried updating the collectionView.layoutMargins, but that did nothing to how the cells are laid out. It seems like epoxy ignores the layoutMargins.

Second, I tried modifying my UICollectionViewCompositionalLayout and modified the group content insets as such:
group.contentInsets = .init(top: 20, leading: 20, bottom: 0, trailing: 20)
This seemed to work for the leading and trailing sides, but the top had no effect on pushing the first cell down.

Any insight on how to lower my top cell without adding a margin to the first cell itself?

Create `ZGroup`

SwiftUI has HStack, VStack, and ZStack while LayoutGroups only has HGroup and VGroup. As an enhancement, we could implement ZGroup that models ZStack

Use reconfigure API to apply updates CollectionView on iOS 15

As discussed in this thread:

When using cell prefetching, watch out for cases where the data for a cell changes after the cell has been prefetched, but before it becomes visible (thus it won't be included in visibleCells).

The current logic relies on cellForItem(at:), which is documented as:

Gets the visible cell object at the specified index path.

As such, this method may not necessarily return a cell instance dequeued via prefetching when applying updates. We should verify whether this is the case or not, as this could potentially result in inconsistent cell data.

How should the child view be horizontally aligned with the parent view?

I'm trying to emulate the system's navigation bar using EpoxyLayoutGroup.

On the system navigation bar, the title is horizontally centered with respect to the parent view, and the left and right views do not affect its position.

How can I use EpoxyLayoutGroup to achieve this feature? Or can I only use AutoLayout to achieve it?

What is the best practice for passing variables between controllers?

I have a scenario:

Page B is a detail page, and its data comes from its parent page which is page A.Also this data can be edited in page A.

According to the example code in MainViewController, I don't understand how to implement my requirement?

I also have a problem: how to implement the common proxy pattern in UIKit in expory? I want to set the proxy of page B to page A, how do I write my code?

How to refresh the data in the list?

I have a view that follows the EpoxyableView protocol, where Content is a String type value.

My page contains 2 different states and I want to clear the value of the page in the previous state when I switch states.

Before calling the setItems method, I have modified the value of the string variable assigned to Content. But when the setItems method ends, I find that my page is still the same as before and the setContent method is not executed.

{V|H}Group not updating when styles of inside items change. (i.e update textColor for a label groupItem in VGroupView)

Hi Epoxy team!

I encountered a problem when playing around with the library, so I created some groupItem views inside a VGroupView and when I update the styles of the items inside the VGroupView (i.e. change textColor for a label added in the VGroup), the view will not be updated. However, change the content of the item does triggering view updates (I guess this makes much sense as there are recursive diff operations). Wondering did I miss anything here? How should I trigger VGroup updates when updating the styles of the inside components?

Thank you so much for the help!

Add accessibilityAlignment support to VGroup items

There are valid use cases for VGroup to support accessibilityAlignment of items and there's no reason we shouldn't support it. For example, you might want to have labels be trailing aligned in standard sizes, and leading aligned for accessibility sizes.

Gesture conflict

While add a tap gesture to dismiss keyboard for collectionview, cell select action do not working.

BarContainerInsetBehavior not able to set through installer.

Hi team! Seems like BarContainerInsetBehavior (and other properties) in TopBarContainer are not able to set through bar installer? I looked through the code and they are indeed public, are there any plans adding these config in a configuration file or in the installer class?

Thanks!!

How to combine Epoxy and flexLayout?

I want to keep the declarative syntax while adopting FlexLayout. SwiftUI is very unfriendly, the layout is not very customizable, and there are some bugs.
Can Epoxy do it?

Explain in detail the logic of cell reuse in `EpoxyCollectionView`?

EpoxyCollectionView works very well and I've tried to build many pages with it, but I'm still confused about one thing: what does the reuse mechanism in Epoxy actually look like? When I provide what kind of data, it will update the list?

I know that dequeue is triggered when dataID and Style change, but I've been confused about Content related content.

Suppose there is a scenario: a list (EpoxyCollectionView) with a lot of data, with a UITextField on each Cell, which needs to listen to user input, while considering the reuse logic.

I got confused in this situation. I listened for user input inside behavior, then I tried to set the content for UITextField in content, but it had some problems when reuse occurred in cell, then I changed to use setBehaviors to assign a value to textField.text instead of the previous content, but it still has problems in some "wrong usage".

I'll provide a demo later if needed to show my "misuse".

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.