Compose is a library that helps you compose complex and dynamic views.
Clone or download
brunogb Merge pull request #7 from VivaReal/HotFix/StackUnit
fixing layout issue with stack unit
Latest commit c05888e Dec 27, 2016
Permalink
Failed to load latest commit information.
Compose.xcodeproj removing unused collectionViewCell Dec 13, 2016
Compose preventing the collectionView from animating some non-animated layouting Dec 15, 2016
ComposePlayground Initial commit Nov 1, 2016
ComposeTests Change version number: 1.0.1 Nov 18, 2016
Documentation improving docs and fixing travis Nov 3, 2016
Example changing podspec version Dec 13, 2016
docs updating shields Nov 3, 2016
fastlane add Dangerfile and bundle Nov 14, 2016
.gitignore add Dangerfile and bundle Nov 14, 2016
.jazzy.yaml improving docs and fixing travis Nov 3, 2016
.swift-version defining swift version to publish pod Nov 1, 2016
.travis.yml add Dangerfile and bundle Nov 14, 2016
CONTRIBUTING.md add Dangerfile and bundle Nov 14, 2016
Compose.podspec fixing layout issue with stack unit, where if could offset the view w… Dec 15, 2016
Dangerfile add Dangerfile and bundle Nov 14, 2016
Gemfile add Dangerfile and bundle Nov 14, 2016
Gemfile.lock changing podspec version Dec 13, 2016
LICENSE
README.md updating shields Nov 3, 2016
circle.yml

README.md

Pod Platform Language Build Status Pod License

Version Carthage Compatible

Compose is a data driven library that will help compose your complex and dynamic Views.

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.

First you will create units that encapsulates the data they will display

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 ComposingUnits and assign it to a state property of a container.

So, what are ComposingUnits?

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)
}

Handling selection and other delegates callbacks

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

Grouping units to represent a single unit

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)
}

Dynamic cell size calculation

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.

ComposingContainer

In order to display an array of ComposingUnits 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.

Tests

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)

Examples

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

Installation

Cocoapods

Compose is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod "Compose"

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

Manual

You can download this repo, drag the Compose.xcodeproj inside your project and link the Compose framework

Documentation

You can find all documentation about Compose here: Documentation

Author

Bruno Bilescky, brunogb@gmail.com

License

Compose is available under the MIT license. See the LICENSE file for more info.