BrickKit: a delightful layout library for iOS and tvOS written in Swift
BrickKit is a delightful layout library for iOS and tvOS. It is written entirely in Swift!
Deprecated
BrickKit is being phased out at Wayfair, and therefore we will not be maintaining it further.
With BrickKit, you can create complex and responsive layouts in a simple way. It's easy to use and easy to extend. Create your own reusable bricks and behaviors.
Define your layouts using objects that describe the high-level behavior
let section = BrickSection(bricks: [
LabelBrick(width: .ratio(ratio: 1), text: "BRICK 1"),
LabelBrick(width: .ratio(ratio: 1), text: "MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK MULTI-LINE BRICK"),
LabelBrick(width: .ratio(ratio: 1/2), text: "1/2 BRICK"),
LabelBrick(width: .ratio(ratio: 1/2), text: "1/2 BRICK"),
LabelBrick(width: .ratio(ratio: 1/3), text: "1/3 BRICK"),
LabelBrick(width: .ratio(ratio: 1/3), text: "1/3 BRICK"),
LabelBrick(width: .ratio(ratio: 1/3), text: "1/3 BRICK"),
], inset: 10, edgeInsets: UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20))
self.setSection(section)
Layout in portrait
Layout in landscape
Define heights based on rules
// Calculate height using auto-layout
LabelBrick(height: .auto(estimate: .fixed(size: 50)), text: "BRICK"),
// Fixed Height
LabelBrick(height: .fixed(size: 50), text: "BRICK"),
// Calculate height based on ratio of the width
LabelBrick(height: .ratio(ratio: 1), text: "BRICK"),
// Complex Rule based on size classes and/or orientation
let height: BrickDimension =
.horizontalSizeClass(
regular: .orientation(
landscape: .fixed(size: 200),
portrait: .fixed(size: 100)
),
compact: .orientation(
landscape: .fixed(size: 100),
portrait: .fixed(size: 50)
)
)
Height Ratio portrait
Height Ratio landscape
Complex layouts, using Sections in Sections
let section = BrickSection(bricks: [
LabelBrick(width: .ratio(ratio: 0.5), text: "BRICK"),
BrickSection(width: .ratio(ratio: 0.5), bricks: [
LabelBrick(text: "BRICK\nBRICK"),
LabelBrick(text: "BRICK"),
LabelBrick(text: "BRICK"),
], inset: 10),
BrickSection(bricks: [
BrickSection(width: .ratio(ratio: 1/3), bricks: [
LabelBrick(text: "BRICK"),
LabelBrick(text: "BRICK"),
], inset: 5),
BrickSection(width: .ratio(ratio: 2/3), backgroundColor: .brickGray3, bricks: [
LabelBrick(text: "BRICK"),
LabelBrick(text: "BRICK"),
LabelBrick(text: "BRICK"),
], inset: 15),
], inset: 5, edgeInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)),
BrickSection(width: .ratio(ratio: 0.5), bricks: [
LabelBrick(text: "BRICK"),
LabelBrick(text: "BRICK"),
], inset: 10),
LabelBrick(width: .ratio(ratio: 0.5), text: "BRICK"),
LabelBrick(text: "BRICK"),
], inset: 10, edgeInsets: UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20))
self.setSection(section)
Example of Section in Section
Create your own reuseable Bricks!
registerBrickClass(NiblessBrick.self)
let section = BrickSection(bricks: [
NiblessBrick(text: "BRICK", image: UIImage(named: "logo_splash")!),
NiblessBrick(width: .ratio(ratio: 1/2), text: "BRICK", image: UIImage(named: "logo_inapp")!),
NiblessBrick(width: .ratio(ratio: 1/2), text: "BRICK", image: UIImage(named: "logo_inapp")!)
], inset: 10)
setSection(section)
BrickKit supports Bricks with Nibs or Nibsless
Use different scroll directions within bricks
self.registerBrickClass(CollectionBrick.self)
self.registerBrickClass(LabelBrick.self)
let section1 = BrickSection(bricks: [
ImageBrick(width: .ratio(ratio: 1/4), height: .ratio(ratio: 1), dataSource: self),
] , inset: 10, edgeInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
section1.repeatCountDataSource = self
let section2 = BrickSection(bricks: [
ImageBrick(width: .ratio(ratio: 1/2), height: .ratio(ratio: 1), dataSource: self),
], inset: 10, edgeInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
section2.repeatCountDataSource = self
let section3 = BrickSection(bricks: [
ImageBrick(width: .fixed(size: 100), height: .ratio(ratio: 1), dataSource: self),
], inset: 10, edgeInsets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
section3.repeatCountDataSource = self
let section = BrickSection(bricks: [
LabelBrick(text: "1/4 Ratio"),
CollectionBrick(scrollDirection: .horizontal, dataSource: CollectionBrickCellModel(section: section1), brickTypes: [ImageBrick.self]),
LabelBrick(text: "1/2 Ratio"),
CollectionBrick(scrollDirection: .horizontal, dataSource: CollectionBrickCellModel(section: section2), brickTypes: [ImageBrick.self]),
LabelBrick(text: "100px Fixed"),
CollectionBrick(scrollDirection: .horizontal, dataSource: CollectionBrickCellModel(section: section3), brickTypes: [ImageBrick.self]),
])
setSection(section)
Dynamic Image Heights
Images are automatically resized based on its content
registerBrickClass(ImageBrick.self)
registerBrickClass(LabelBrick.self)
let imageURL = NSURL(string:"https://secure.img2.wfrcdn.com/lf/8/hash/2664/10628031/1/custom_image.jpg")!
let section = BrickSection("RootSection", bricks: [
LabelBrick(text: "Below is an image brick with fixed height"),
ImageBrick(height: .fixed(size: 50)dataSource: ImageURLBrickModel(url: imageURL, contentMode: .ScaleAspectFit)),
LabelBrick(text: "Below is an image brick loaded dynamically"),
ImageBrick(height: .auto(estimate: .fixed(size: 50)), dataSource: ImageURLBrickModel(url: imageURL, contentMode: .ScaleAspectFit)),
LabelBrick(text: "Below is an image brick with fixed height"),
ImageBrick(height: .fixed(size: 50), dataSource: ImageURLBrickModel(url: imageURL, contentMode: .scaleAspectFill)),
], inset: 10)
self.setSection(section)
Advanced Sticky
...
behavior = StickyLayoutBehavior(dataSource: self)
...
func stickyLayoutBehavior(stickyLayoutBehavior: StickyLayoutBehavior, shouldStickItemAtIndexPath indexPath: NSIndexPath, withIdentifier identifier: String, inCollectionViewLayout collectionViewLayout: UICollectionViewLayout) -> Bool {
return identifier == BrickIdentifiers.titleLabel
}
Coverflow
layout.scrollDirection = .horizontal
let snapToBehavior = SnapToPointLayoutBehavior(scrollDirection: .horizontal(.Center))
self.brickCollectionView.layout.behaviors.insert(snapToBehavior)
self.brickCollectionView.layout.behaviors.insert(CoverFlowLayoutBehavior(minimumScaleFactor: 0.75))
Appear Behavior (none, bottom, top)
layout.appearBehavior = BrickAppearTopBehavior() // Insert from the top
layout.appearBehavior = BrickAppearBottomBehavior() // Insert from the bottom
Spotlight
...
behavior = SpotlightLayoutBehavior(dataSource: self)
...
func spotlightLayoutBehavior(behavior: SpotlightLayoutBehavior, smallHeightForItemAtIndexPath indexPath: NSIndexPath, withIdentifier identifier: String, inCollectionViewLayout collectionViewLayout: UICollectionViewLayout) -> CGFloat? {
return identifier == BrickIdentifiers.repeatLabel ? 50 : nil
}
Features
- Setup is simple, can create a view with a few lines of code.
- Is an alternative implementation of a UICollectionViewController.
- Advanced Width and Height setups, views can be sized according to iPad/screen size upon initialization!
- Allows for the reuse of cells.
- Supports multiple nibs per BrickCell: UICollectionViewCell class.
- Has "behaviors", which are complex layout interactions that can be applied to any view controller.
- Sticky Headers/Footers
- Hide Views
- Scrolling Behaviors
- Snap to Point
- These are all supported by ten lines or less of code!
- Asynchronous changing of height
- Allows content to flow in after the cell is already on screen.
- Supports the height of cells to resize after an image or video has loaded.
Installation Instructions
Installation is simple. You just need Cocoapods and it is a matter of adding this to your podfile:
pod 'BrickKit'
See [Installation Details](#Installation Details)
Terminology
Brick
The Brick
object represents the model of what needs to be laid out on screen. E.g. in case of a LabelBrick
, the model should contain the text that should go inside the label.
We strongly encourage that every
Brick
has adatasource
and an optionaldelegate
. This is a pattern that proves to be successful and allows your brick to be repeated.
For simple bricks, it is suggested to create a basic BrickModel that subclasses that datasource, so that you do not have to implement the datasource in the viewcontroller.
For a number of examples on how to implement Bricks, refer to the examples in the BrickKit pod, such asLabelBrick
orButtonBrick
.
BrickSection
The BrickSection
provides information for the BrickCollectionView on how to lay out the cells on screen. In particular, it provides the identifiers
,
the bricks inside of the BrickSection, as well as UIEdgeInsets
and an inset
.
A
BrickSection
is a subclass of a Brick. This provides for nesting of multiple BrickSections inside of a BrickSection. The BrickCollectionView will only accept a single, unified
BrickSection, so if you want complex layouts you will want to nest BrickSections together.
BrickCell
The BrickCell
object is representing the actual view. The cell will be instantiated when the view comes on screen and will call its dataSource to obtain information on how to
display itself.
As BrickKit is based on
UICollectionView
, everyBrickCell
is a subclass ofUICollectionViewCell
.
BrickCollectionView
A BrickCollectionView, a subclass of UICollectionView
, manages the brick section/sections. It is also responsible for loading the correct BrickCell
for a given brick.
You'll need to register every brick that is used in the BrickCollectionView. (
brickCollectionView.registerBrickClass(LabelBrick.self)
)
If you fail to do so, the app will crash (with a descriptive message)
Note, a CollectionBrick is a brick that houses a BrickCollectionView. If you implement one of these, you will have to register the brickClass in the CollectionBrick's BrickCollectionView or you will crash with a nib not found error.
BrickLayout
An object that implements the BrickLayout
protocol is responsible for the displaying of the BrickCells on the BrickCollectionView.
BrickFlowLayout
A BrickFlowLayout is an implementation of the BrickLayout protocol.
A BrickFlowLayout is a subclass of a UICollectionViewLayout.
BrickDimension
A BrickDimension
is something you provide upon the initialization of a brick. This is where the magic happens.
BrickDimensions allow for widths and heights to be based off of view size or a fixed value. Then can then also be dependent on your horizontal or vertical size class, your orientation, or you can
simply leave it as automatic and let auto layout do its thing.
BrickRepeatCountDataSource
A BrickRepeatCountDataSource
allows you to specify a repeat count for a certain brick. This will give your BrickCell
an index, similar to how arrays are setup.
Implementation Details
Ideally, a Brick has an identifier. This identifier can be used to refer back to a certain Brick.
The advantage of this over using NSIndexPath
is that it decouples the model from the layout.
This allows you to use the same brick, datasource, and delegate for a cell.
BrickKit comes with some useful default brick implementations:
The root object used in BrickKit is a BrickSection
The following code will create a label that is:
- The full width of the view.
- Its height will dynamically resize based on the text that is inside of the label.
- The above two conditions are dependent on the nib's constraints being setup correctly, without any specific heights or width set.
--------------------------------
| LABEL BRICK |
--------------------------------
import UIKit
import BrickKit
class ViewController: BrickViewController {
override func viewDidLoad() {
super.viewDidLoad()
let section = BrickSection(bricks: [
LabelBrick(text: "LABEL BRICK")
])
self.setSection(section)
}
}
Bricks are sized using auto layout. If the constraints are setup to grow vertically, the brick will get the proper height as needed.
So in case of a label, when the text is larger than the width, it will use the next line.
--------------------------------
| LABEL BRICK LABEL BRICK LABEL |
| BRICK LABEL BRICK |
--------------------------------
let section = BrickSection(bricks: [
LabelBrick(text: "LABEL BRICK LABEL BRICK LABEL BRICK LABEL BRICK")
])
Try it in the LabelBrickPlayground.
BrickDimension
To setup the dimensions of a brick, please refer to BRICK_DIMENSION.md.
BrickRepeatCountDataSource
BrickRepeatCountDataSources are setup based off a section. They can be set upon any BrickSection
.
import UIKit
import BrickKit
class ViewController: BrickViewController {
var fruits: [Fruit] = []
override func viewDidLoad() {
super.viewDidLoad()
fruits.append(Fruit(name: "Apple"))
fruits.append(Fruit(name: "Banana"))
fruits.append(Fruit(name: "Cherry"))
let section = BrickSection(bricks: [
LabelBrick("FRUIT", text: "BRICK 1")
])
section.repeatCountDataSource = self
self.setSection(section)
}
}
extension ViewController: BrickRepeatCountDataSource {
func repeatCount(for identifier: String, with collectionIndex: Int, collectionIdentifier: String) -> Int {
return identifier == "FRUIT" ? fruits.count : 1
}
}
Try it in the BrickRepeatCountPlayground.
Layout
HideBehavior
This behavior allows you to hide bricks and sections.
BrickAppearBehavior
A behavior that defines the direction where bricks are appearing and disappearing from.
BrickAppearBehavior
is extendable, so you can create your own behavior
BrickLayoutZIndexBehavior
A behavior that determines how the bricks are laid out on the z-Axis. This is very important when dealing with behaviors that allow intersections between bricks
Behavior | Description |
---|---|
TopDown | The cell at the top has the highest zIndex. Ideal for layouts that need Sticky cells, where a Brick needs to be displayed above all other bricks. |
BottomUp | The cell at the bottom has the highest zIndex. Ideal for layouts where the lower cells are above the higher cells. |
Behaviors
A behavior changes the way a brick is displayed on screen. This can depend on scrolling, refreshing etc
BrickKit comes with a few behaviors built in:
Behavior | Description |
---|---|
StickyLayoutBehavior | Allows bricks and sections to stick to the top of the screen. The brick will stick until its containing section is passed. |
MinimumStickyLayoutBehavior | Same as the StickyLayoutBehavior , but the height of the brick will first shrink to a minimum value before it starts sticking. |
StickyFooterLayoutBehavior | Allows bricks and sections to stick to the bottom of the screen. The brick will stick until its containing section is passed. |
OnScrollDownStickyLayoutBehavior | Same as the StickyLayoutBehavior , but the bricks will only stick when scrolling back up. When scrolling down, the brick will just flow with the other bricks. |
SnapToPointLayoutBehavior | After scrolling, this behavior will snap your ScrollView to a given location. This is useful if you want bricks to snap to a point after the user is done scrolling. |
SpotlightLayoutBehavior | While scrolling, the brick 'in the spotlight' will grow. Only one brick is in the spotlight at a time. |
CardLayoutBehavior | The bricks will scroll like a stack of cards. The ScrollView will not scroll until one card is completely on top of the other. |
CoverFlowLayoutBehavior | While scrolling, the brick closest to the center of the screen will grow bigger while the other bricks will shrink. |
OffsetLayoutBehavior | Allows an offset to the origin or size of bricks. |
MaxZIndexLayoutBehavior | Allows a brick to be set to the maxZIndex, which means they will always be on top of any other brick. |
SetZIndexLayoutBehavior | Allows the layout to set the ZIndex of any brick to any value. |
BrickLayoutBehavior
is extendable, so are free to create your own behaviors.
Requirements
- iOS 9.1+ / tvOS 9.0+
- Xcode 8.0+
- Swift 3.0 (Swift 3.2 compatible)
Communication
- If you found a bug, open an issue.
- If you have a feature request, open an issue.
- If you want to contribute, submit a pull request.
Installation Details
CocoaPods
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
$ gem install cocoapods
To integrate BrickKit into your Xcode project using CocoaPods, specify it in your Podfile
:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'BrickKit'
end
Then, run the following command:
$ pod install
Carthage
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with Homebrew using the following command:
$ brew update
$ brew install carthage
To integrate BrickKit into your Xcode project using Carthage, specify it in your Cartfile
:
github "wayfair/brickkit-ios"
Run carthage update
to build the framework and drag the built BrickKit.framework
into your Xcode project.
Credits
BrickKit is owned and maintained by Wayfair.
Code of Conduct
See CODE_OF_CONDUCT.md.
Contributing
See CONTRIBUTING.md.
License
BrickKit is released under the Apache license. See LICENSE for details.