It helps you create complex and dynamic Views using easy and simple composition of data structures. It is really easy to use and extend.
And it also is a breeze to implement on existing projects.
let info = "Info to be displayed"
let infoUnit = LabelUnit(text: info, traits: [.height(40)])
and then, you can add this unit to a container:
container.state = [infoUnit]
you can also conditionaly add this unit to the container, like:
var units: [ComposingUnit] = [....]
units.add(if: someCondition) {
return LabelUnit(text: info, traits: [.height(40)])
}
container.state = units
So, instead of dealing with many dataSource/delegate methods you can just create an array of ComposingUnit
s and assign it to a state
property of a container.
The protocol ComposingUnit is the heart of this framework.
You can make any class or structure conform to it, taking the advantages of Value Type
and Reference Type
when they best suits your needs.
Also, this class/structure should hold all the data that it will display. With this approach, we don't need to hold a reference to the models that generated these units.
Let's say we want to display a list of all the feed items we have.
let feedItems: [FeedItem] = ... //You can grab an array from CoreData, JSON, Realm, anywhere...
var feedUnits: [ComposingUnit] = feedItems.map { FeedUnit(id: $0.uniqueId, title: $0.title, image: $0.image, likeCount:Int) }
container.state = feedUnits
So after we create the feedUnits
array we don't need feedItems
anymore and we can easily use our feedUnits in any thread.
And we can add any other ComposingUnit
to this array, allowing us to display a view totally different to a feed item in the same list
feedUnits.add(if: feedUnits.count > 4) {
return SeeMoreFeedsUnit(feedsCount: feedUnits.count)
}
To handle cell selection or other delegate callbacks, all your class/structure has to do is implement an applicable Protocol
. For cell selection it is the SelectableUnit
protocol. This protocol defines a method that will be called once the cell has been selected.
You can check all extension protocols here
You can also use a CollectionStackUnit to group some units together as a single unit
var units: [ComposingUnit] = [...] //create somewhere
units.add(ifLet: data.optionalFeedItem) { feedItem in
var innerUnits: [ComposingUnit] = [HeaderUnit(text: feedItem.title)]
innerUnits.add(ifLet: feedItem.image) { image in
return FeedImageUnit(image: image)
}
innerUnits.add(ifLet: feedItem.video) { video in
return FeedVideoUnit(video: video)
}
innerUnits.append(ActionBarUnit(likes: feedItem.likes, comments: feedItem.comments))
let unit = CollectionStackUnit(identifier: feedItem.uniqueIdentifier, direction: .vertical, traits: [], units: innerUnits)
return unit
}
Maybe you could also create a function that returns this item based on a FeedItem
func FeedUnit(from: FeedItem)-> CollectionStackUnit {
var innerUnits: [ComposingUnit] = [HeaderUnit(text: feedItem.title)]
innerUnits.add(ifLet: feedItem.image) { image in
return FeedImageUnit(image: image)
}
innerUnits.add(ifLet: feedItem.video) { video in
return FeedVideoUnit(video: video)
}
innerUnits.append(ActionBarUnit(likes: feedItem.likes, comments: feedItem.comments))
let unit = CollectionStackUnit(identifier: feedItem.uniqueIdentifier, direction: .vertical, traits: [], units: innerUnits)
return unit
}
var units: [ComposingUnit] = [...] //create somewhere
units.add(ifLet: data.optionalFeedItem) { feedItem in
return FeedUnit(from: feedItem)
}
We use a struct called DimensionUnit to represent a cell width/height calculation. A DimensionUnit
can calculate a dimension using:
- Static values: It will ignore it's container size and always return this static value
- Percent based values: It will return a percentual of it's container dimension
- Total value based: It will return it's container dimension minus a static value
- Custom based: It will execute a closure passing it's container size as parameter.
Using DimensionUnit
we can easily express our units height and width.
In order to display an array of ComposingUnit
s you will need an UIView that conforms to ComposingContainer
.
We provide two default containers in the framework: ComposingCollectionView
and ComposingTableView
. Both have automatic detection of inserts/updates/deletes in their state they are displaying.
It gets really simple to test your interface, as you can test the presence of some specific unit, and you don't need to render your interface.
let dummyItem = FeedItem(...)
var feedUnit = FeedUnit(from: dummyItem)
XCTAssert(feedUnit.units.count == 3)
dummyItem.image = nil
dummyItem.video = nil
feedUnit = FeedUnit(from: dummyItem)
XCTAssert(feedUnit.units.count == 2)
We provide some cool examples in our Example project.
To run, clone this repo, and open the Example/Compose_Example.xcodeproj
. You don't need to do any pod install or any configuration to run this project
Compose is available through CocoaPods. To install
it, simply add the following line to your Podfile
:
pod "Compose"
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 Compose into your Xcode project using Carthage, specify it in your Cartfile
:
github "VivaReal/Compose" ~> 1.0
Run carthage update
to build the framework and drag the built Compose.framework
into your Xcode project.
You can download this repo, drag the Compose.xcodeproj
inside your project and link the Compose
framework
You can find all documentation about Compose here: Documentation
Bruno Bilescky, [email protected]
Compose is available under the MIT license. See the LICENSE file for more info.