Declarative view components
ViewComponents is a library that helps you to create View Models that are:
- Declarative
- Composable
- Efficient
- Perfect fit to MVVM architecture
- Easy to test
- Easy to support custom views and types
You describe how the view should look like and the library will take care to applying the styling. Here's an example:
let buttonComponent = Component<UIButton>(
.title("Test", for: .normal),
.titleColor(.red, for: .normal)
)
let myButton: UIButton /// The button we want to style
buttonComponent.configure(view: myCustomeView)
No more manually setting the views properties!
class MyCustomView: UIView {
@IBOutlet var myLabel: UILabel!
@IBOutlet var myButton: UIButton!
@IBOutlet var myIcon: UIImageView!
}
With ViewComponents you can easily create immutable values that represent the styling of the whole view hierarchy like this:
let labelComponent = Component<UILabel>(
.text("MyLabel"),
.font(.boldSystemFont(ofSize: 12)),
.backgroundColor(.blue)
)
let viewComponent = Component<MyCustomView>(
.border(cornerRadius: 12, width: 3, color: .red)
).withChildren(
.child({ $0.myButton }, styles:
.title("Test", for: .normal),
.titleColor(.red, for: .normal)
),
.child({ $0.myLabel }, labelComponent),
.child({ $0.myIcon }, styles:
.image(myImage)
)
)
and then when we need to apply our style we do the following:
viewComponent.configure(view: myCustomeView)
The library provides diffing mechanism. Consider following 2 components
let button1 = Component<UIButton>(
.title("Test", for: .normal),
.titleColor(.red, for: .normal)
)
let button2 = Component<UIButton>(
.title("Test", for: .normal),
.titleColor(.blue, for: .normal),
.adjustsImageWhenHighlighted(true)
)
We can apply only the difference to the view:
let changes = button1.diffChanges(from: button2)
Which in this case will be the following component:
let realChanges = Component<UIButton>(
.titleColor(.blue, for: .normal),
.adjustsImageWhenHighlighted(true)
)
The diffing will take the diffs for the all the subcomponents as well
Library provides a protocol ComponentConvertible
that your ViewModels should can conform to and then it's easy to apply ViewModel to the the view. Here is an example of MVVM architecture from here.
Imagine we have the following model:
struct Person {
let salutation: String
let firstName: String
let lastName: String
let birthday: Date
}
And we have the following view:
class PersonView: UIView {
@IBOutlet var nameLabel: UILabel!
@IBOutlet var birthdateLabel: UILabel!
}
Now the View Model will look like this:
struct PersonViewModel: ComponentConvertible {
let person: Person
var name: String {
return "\(person.salutation) \(person.firstName) \(person.lastName)"
}
var birthday: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE MMMM d, yyyy"
return formatter.string(from: person.birthday)
}
var toComponent: Component<PersonView> {
return Component(
.border(width: 12, color: .red)
).withChildren(
.child({ $0.nameLabel }, styles:
.text(name),
.font(.boldSystemFont(ofSize: 12)),
.textColor(.red)
),
.child({ $0.birthdateLabel }, styles:
.backgroundColor(.yellow), .alpha(0.8),
.font(.systemFont(ofSize: 10)), .text(birthday)
)
)
}
}
And let's apply it:
let person: Person /// our Person model here
let view: PersonView /// our view
PersonViewModel(person: person).configure(view: view)
That's it!)
In you unit tests, you just need to check for the equality the ViewModels
toComponent
value and your desired Component
. And that's it. Something like the following:
let person = Person(salutation: "Hallo", firstName: "John", lastName: "Doe", birthday: Date())
let target = Component<PersonView>(
.border(width: 12, color: .red)
).withChildren(
.child({ $0.nameLabel }, styles:
.text("Hallo John Doe"),
.font(.boldSystemFont(ofSize: 12)),
.textColor(.red)
),
.child({ $0.birthdateLabel }, styles:
.backgroundColor(.yellow), .alpha(0.8),
.font(.systemFont(ofSize: 10)), .text("Tuesday May 16, 2017")
)
)
XCTAssertEqual(PersonViewModel(person: person).toComponent, target)
Let's imagine that you have the following custom class with a property isShiny
:
class MyCustomView: UIView {
var isShiny: Bool = true {
didSet {
// Do some magic stuff here
}
}
}
In order for ViewComponents to support MyCustomView
we need to add the following code:
private enum MyCustomViewStyleKey: Int {
case isShiny
}
extension AnyStyle where T: MyCustomView {
private typealias ViewStyle<Item> = Style<T, Item, MyCustomViewStyleKey>
static func isShiny(_ value: Bool) -> AnyStyle<T> {
return ViewStyle(value, key: .isShiny, sideEffect: { $0.isShiny = $1 }).toAnyStyle
}
}
Let's take a closer look at what we did:
- We need to declare a
Int enum
that will contain the keys for every propery we want to support. In our case it'sMyCustomViewStyleKey
- We need to make an extension to
AnyStyle
where generic type isMyCustomView
or it's subclass - For convenienve we define
typealias ViewStyle<Item>
- We need to implement static function that will contain our side effect. We provide the value (in our case it's a
Bool
), key (in our caseMyCustomViewStyleKey.isShiny
) and a side effect function, that take 2 parameters: the view of typeT
and the value which is boolean in our case.
And that's it.
Now we can use our MyCustomView
just as any other type:
let customView = Component<MyCustomView>(
.isShiny(true)
)