Swift library for building component-based interfaces on top of UITableView and UICollectionView 🍱
Clone or download
diegopetrucci Merge pull request #73 from Babylonpartners/show-cancel-animation
Show search bar cancel button with animation
Latest commit 334214d Nov 19, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci Rename the Bento Example app scheme. Oct 30, 2018
Bento.xcodeproj Add MenuItemResponding to support long press to show custom menu actions Nov 6, 2018
Bento.xcworkspace Fixed build settings & added missing schemes. Oct 30, 2018
Bento Check if indexpath if nil Nov 9, 2018
BentoKit Merge pull request #73 from Babylonpartners/show-cancel-animation Nov 19, 2018
BentoTests Support collection supplementary views. (#42) Aug 18, 2018
BuildConfigs Include build configurations to be shared across modules Oct 24, 2018
Carthage/Checkouts Update the dependencies. Oct 30, 2018
Example Custom Adapter & Component Lifecycle Messages for UICollectionView. Nov 2, 2018
Resources Bento 0.1 Apr 13, 2018
Snapshots/ReferenceImages_64 consider accessory view real size when calculating height, added snap… Nov 16, 2018
StyleSheets move showsCancelButton from stylesheet to component Nov 19, 2018
script Stop specifying `-sdk` which fiddles with test builds. Oct 30, 2018
.gitattributes Keep the wildcard expression Oct 24, 2018
.gitignore Introduce BentoKit Oct 24, 2018
.gitmodules Introduce BentoKit Oct 24, 2018
Bento.podspec Update the podspecs. Oct 26, 2018
BentoKit.podspec Revert to using `resource_bundles` in the podspec. Nov 7, 2018
Cartfile Update the dependencies. Oct 30, 2018
Cartfile.private Update the dependencies. Oct 30, 2018
Cartfile.resolved Update the dependencies. Oct 30, 2018
Gemfile Bento 0.1 Apr 13, 2018
LICENSE Bento 0.1 Apr 13, 2018
README.md Custom Adapter & Component Lifecycle Messages for UICollectionView. Nov 2, 2018
StyleSheets.podspec Update the podspecs. Oct 26, 2018

README.md

Bento 🍱 弁当

is a single-portion take-out or home-packed meal common in Japanese cuisine. A traditional bento holds rice or noodles, fish or meat, with pickled and cooked vegetables, in a box.

Bento is a Swift library for building component-based interfaces on top of UITableView.

  • Declarative: provides a painless approach for building UITableView interfaces
  • Diffing: reloads your UI with beautiful animations when your data changes
  • Component-based: Design reusable components and share your custom UI across multiple screens of your app

In our experience it makes UI-related code easier to build and maintain. Our aim is to make the UI a function of state (i.e: UI = f(state)), which makes Bento a perfect fit for Reactive Programming.

Content 📋

What's it like? 🧐

When building a Box, all you need to care about are Sectionss and Nodes.

let box = Box<SectionId, RowId>.empty
                |-+ Section(id: SectionId.user,
                            header: EmptySpaceComponent(height: 24, color: .clear))
                |---+ RowId.user <> IconTitleDetailsComponent(icon: image, title: patient.name)
                |-+ Section(id: SectionId.consultantDate,
                            header: EmptySpaceComponent(height: 24, color: .clear))
                |---+ RowId.loading <> LoadingIndicatorComponent(isLoading: true)

tableView.render(box)

How does it work? 🤔

Setup

Bento automatically performs the data source and delegate setup upon the very first time UITableView or UICollectionView is asked to render a Bento Box.

In other words, for Bento to work, it cannot be overriden with your own data source and delegate. If you wish to respond to delegate messages which Bento does not support as a feature, you may consider supplying a custom adapter using prepareForBoxRendering(_:).

Collection View Adapter Base Class Required Protocol Conformances
UITableView TableViewAdapterBase UITableViewDataSource & UITableViewDelegate
UICollectionView CollectionViewAdapterBase UITableViewDataSource & UITableViewDelegate

Box 📦

Box is a fundamental component of the library, essentially a virtual representation of the UITableView content. It has two generic parameters - SectionId and RowId - which are unique identifiers for Section<SectionId, RowId> and Node<RowId>, used by the diffing engine to perform animated changes of the UITableView content.

Sections and Nodes 🏗

Sections and Nodes are building blocks of a Box:

  • Section is an abstraction of UITableView's section which defines whether a header or footer should be shown.
  • Node is an abstraction of UITableView's row which defines how the data is rendered.
struct Section<SectionId: Hashable, RowId: Hashable> {
    let id: SectionId
    let header: AnyRenderable?
    let footer: AnyRenderable?
    let rows: [Node<RowId>]
}

public struct Node<Identifier: Hashable> {
    let id: Identifier
    let component: AnyRenderable
}

Identity 🎫

Identity, one of the key concepts, is used by the diffing algorithm to perform changes.

For general business concerns, full inequality of two instances does not necessarily mean inequality in terms of identity — it just means the data being held has changed if the identity of both instances is the same.

(More info here.)

SectionID and ItemID define the identity of sections and their items, respectively.

Renderable 🖼

Renderable is similar to React's Components. It's an abstraction of the real UITableViewCell that is going to be displayed. The idea is to make it possible to create small independent components that can be reused across many parts of your app.

public protocol Renderable: class {
    associatedtype View: UIView

    func render(in view: View)
}

class IconTextComponent: Renderable {
    private let title: String
    private let image: UIImage

    init(image: UIImage,
         title: String) {
        self.image = image
        self.title = title
    }

    func render(in view: IconTextCell) {
        view.titleLabel.text = title
        view.iconView.image = image
    }
}

Bento's arithmetics 💡

There are several custom operators that provide syntax sugar to make it easier to build Bentos:

precedencegroup ComposingPrecedence {
    associativity: left
    higherThan: NodeConcatenationPrecedence
}

precedencegroup NodeConcatenationPrecedence {
    associativity: left
    higherThan: SectionConcatenationPrecedence
}

precedencegroup SectionConcatenationPrecedence {
    associativity: left
    higherThan: AdditionPrecedence
}

infix operator <>: ComposingPrecedence
infix operator |-+: SectionConcatenationPrecedence
infix operator |-?: SectionConcatenationPrecedence
infix operator |---+: NodeConcatenationPrecedence
infix operator |---?: NodeConcatenationPrecedence

let bento = Box.empty // 3
	|-+ Section() // 2
	|---+ RowId.id <> Component() // 1

As you might have noticed:

  • <> has ComposingPrecedence;
  • |---+ has NodeConcatenationPrecedence

<> / NodeConcatenationPrecedence is higher than |-+ / SectionConcatenationPrecedence, meaning Nodes will be computed first.

The order of the expression above is:

  1. RowId.id <> Component() => Node
  2. Section() |---+ Node() => Section
  3. Box() |-+ Section() => Box

Conditional operators ❓

In addition to the |-+ and |---+ concatenation operators, Bento has conditional concatenation operators:

  • |-? for Section
  • |---? for Node

They are used to provide a Section or Node in a closure for the Bool and Optional happy path, via the .iff and .some functions.

Here are some examples:

let box = Box.empty
    |-? .iff(aBoolCondition) {
        Section()  // <-- Section only added if `boolCondition` is `true`
    }
let box = Box.empty
    |-? .some(anOptional) { theOptional in  // <-- the value of anOptional unwrapped
        Section()  // <-- Section only added if `anOptional` is not `nil`
    }

|---? works in exactly the same way for Nodes

Examples 😎

Sections Appointment Movies

Installation 💾

  • Cocoapods
target 'MyApp' do
    pod 'Bento'
end
  • Carthage
github "Babylonpartners/Bento"

Development Installation 🛠

If you want to clone the repo for contributing or for running the example app you will need to install its dependencies which are stored as git submodules:

git submodule update --init --recursive

Or, if you have Carthage installed, you can use it to do the same thing:

carthage checkout

State of the project 🤷‍♂️

Feature Status
UITableView ✅
UICollectionView ✅
Carthage Support ✅
Free functions as alternative to the operators ❌

Contributing ✍️

Contributions are very welcome and highly appreciated! ❤️ Here's how to do it:

  • If you have any questions feel free to create an issue with a question label;
  • If you have a feature request you can create an issue with a Feature request label;
  • If you found a bug feel free to create an issue with a bug label or open a PR with a fix.

Image attributions

Coffee Pomegranate fruit Cherries Strawberries