Epoxy-ios

repository·master·Indexed 23 days ago

https://github.com/airbnb/epoxy-ios

A suite of declarative UI APIs for building UIKit applications in Swift. Inspired by SwiftUI and Android's Epoxy, it provides modular tools to manage UICollectionView content (EpoxyCollectionView), UINavigationController stacks (EpoxyNavigationController), UIViewController modal presentations (EpoxyPresentations), fixed bar stacks (EpoxyBars), and composable layouts using HGroup and VGroup (EpoxyLayoutGroups).

Tokens
2.5K
Snippets
6
Records
9
Agent score
30%

What's inside Epoxy-ios

  1. Overview of Epoxy Modules

    master

    Epoxy uses a modular architecture. You can include the umbrella Epoxy module to get everything, or select specific modules based on your requirements:

    • Epoxy: Includes all modules in a single import.
    • EpoxyCollectionView: Declarative API for UICollectionView content.
    • EpoxyNavigationController: Declarative API for UINavigationController stacks.
    • EpoxyPresentations: Declarative API for UIViewController modal presentations.
    • EpoxyBars: Declarative API for fixed top/bottom bar stacks on a UIViewController.
    • EpoxyLayoutGroups: Declarative API for composable layouts (similar to SwiftUI stacks).
    • EpoxyCore: Foundational APIs used by all other Epoxy modules.
  2. Compose layouts with EpoxyLayoutGroups (HGroup and VGroup)

    master

    EpoxyLayoutGroups are UIKit Auto Layout containers inspired by SwiftUI's HStack and VStack. They allow you to compose elements into horizontal (HGroup) or vertical (VGroup) stacks without creating deep view hierarchies, as they use UILayoutGuide internally.

    Usage Pattern

    1. Define a group using HGroup(spacing:) or VGroup(alignment:spacing:) with a trailing closure containing groupItem calls.
    2. Call group.install(in: view) to add the group to a view hierarchy.
    3. Use group.constrainToMargins() or standard Auto Layout to position the group.

    Groups can be nested using VGroupItem to create complex layouts.

  3. Install Epoxy via Swift Package Manager (SPM)

    master

    To install Epoxy using Swift Package Manager in Xcode:

    1. Select FileSwift PackagesAdd Package Dependency.
    2. Enter the repository URL: https://github.com/airbnb/epoxy-ios.git.

    Epoxy is organized into library products, allowing you to import only the specific modules you need.

    https://github.com/airbnb/epoxy-ios.git
  4. Use EpoxyBars for declarative bar stacks

    master

    EpoxyBars allows you to declaratively render fixed top, fixed bottom, or input accessory bar stacks within a UIViewController.

    To use it, define a property with the @BarModelBuilder attribute that returns [BarModeling]. Then, create an installer (e.g., BottomBarInstaller) and call .install() in viewDidLoad().

    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
          }))
      }
    }
  5. Access Epoxy documentation and examples

    master

    Epoxy provides several resources for learning and implementation:

    • Wiki: Contains full documentation and step-by-step tutorials.
    • DocC Documentation: Hosted on the Swift Package Index for type-level details.
    • Example App: A full sample app is included in the repository. You can run it using the EpoxyExample scheme in Epoxy.xcworkspace or browse the source in the Example directory.
  6. Use EpoxyNavigationController to drive navigation stacks

    master

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

    Subclass NavigationController and use the @NavigationModelBuilder attribute on a property that returns [NavigationModel]. You can define a .root model and conditionally add other NavigationModel instances to the stack. Use setStack(stack, animated: true) to trigger navigation updates.

    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
            })
        }
      }
    }
  7. Use EpoxyPresentations for declarative modal presentations

    master

    EpoxyPresentations provides a declarative API for driving modal presentations of a UIViewController.

    Subclass UIViewController and use the @PresentationModelBuilder attribute on a property that returns an optional PresentationModel?. Use setPresentation(presentation, animated: true) to show or dismiss the modal based on the model's presence.

    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
            })
        }
      }
    }
  8. Use EpoxyCollectionView for declarative UICollectionView content

    master

    EpoxyCollectionView provides a declarative API for driving UICollectionView content. You can use CollectionViewController, a subclassable UIViewController, to manage a collection view.

    For simple use cases, you can instantiate CollectionViewController directly with a layout and an items closure. For more complex state management, subclass CollectionViewController and use the @ItemModelBuilder attribute on a property that returns [ItemModeling]. When the state changes, call setItems(items, animated: true) to update the collection view.

    // Simple instantiation
    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
          }
      })
    
    // Subclassing for stateful content
    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
          }
      }
    }