NamadaLayout
NamadaLayout technically is a DSL framework for Swift to make Auto layout easier. But its more than that.
Example
To run the example project, clone the repo, and run pod install
from the Example directory first.
Requirements
Installation
NamadaLayout is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'NamadaLayout'
Author
Nayanda Haberty, [email protected]
License
NamadaLayout is available under the MIT license. See the LICENSE file for more info.
Example Project
You can see the example project here or for more complex version at https://github.com/nayanda1/Tour-Of-Heroes
Usage
Basic Usage
Here's some example of layouting using NamadaLayout which I think simple enough for anyone to understand how to use it.
class ViewController: UIViewController {
private var clickCount: Int = 0
lazy var bottomButton: UIButton = build {
$0.didClicked { [weak self] _ in
self?.clickCount += 1
self?.topLabel.text = "click count: \(self?.clickCount ?? 0)"
}
$0.setTitle("Bottom Button", for: .normal)
$0.setTitleColor(.black, for: .normal)
}
lazy var topLabel = build(UILabel.self)
.text("TOP LABEL")
.textColor(.black)
.textAlignment(.center)
.lineBreakMode(.byTruncatingMiddle)
.build()
@ViewState var topTitle: String?
@ViewState var typedText: String?
override func viewDidLoad() {
super .viewDidLoad()
layoutView()
}
func layoutView() {
layoutContent { content in
content.putSearchBar()
.placeholder("I am Placeholder")
.at(.fullTop, .equal, to: .safeArea)
.bind(\.text, with: $typedText)
content.put(topLabel)
.top(.equalTo(18), to: Anchor(of: .previous).bottomAnchor)
.centerX(.equal, to: .parent)
.horizontal(.moreThanTo(18), to: .parent)
.bind(\.text, with: $topTitle)
.whenStateChanged(for: $typedText, thenAssign: \.text, with: "typed: \(typedText ?? "null")")
content.putVStack()
.alignment(.center)
.distribution(.fill)
.spacing(9)
.center(.equal, to: .parent)
.inBetween(of: topLabel, and: bottomButton, .vertically(.moreThanTo(18)), priority: .defaultLow)
.horizontal(.moreThanTo(18), to: .parent)
.layoutContent { sContent in
sContent.putStackedLabel()
.text("MIDDLE LABEL")
.textColor(.black)
.textAlignment(.center)
.lineBreakMode(.byTruncatingMiddle)
.observe($topTitle) {
$0.text = "new: \($1.new ?? "null"), old: \($1.old ?? "null")"
}
sContent.putStackedImageView()
.image(#imageLiteral(resourceName: "anyImage"))
.contentMode(.scaleAspectFit)
.size(.equalTo(.init(width: 90, height: 90)))
}
content.put(bottomButton)
.at(.fullBottom, .equalTo(18), to: .safeArea)
.height(.equalTo(36))
}
}
}
It will automatically put view as closure hierarchy, create all constraints inside the closure and activate it just after the closure finished on the UIThread. It will also bind view keypath into any property with same type with @ViewState attributes.
Layouting
Basic
There are two main function to create layout which is:
layout(withDelegate delegate: NamadaLayoutDelegate?, _ options: SublayoutingOption, _ layouter: (ViewLayout<Self>) -> Void)
layoutContent(withDelegate delegate: NamadaLayoutDelegate? = nil, _ options: SublayoutingOption, _ layouter: (LayoutContainer<Self>) -> Void)
all the method can be used in UIViewController and UIView. The difference between those three islayout
are used to layout the Constraints of the view.layoutContent
are used to layout content of the view ignoring it's own constraints.
both accept SubLayoutingOption enumeration which is:
addNew
which will add new constraints ignoring the current constraints of the view, This is the default value and the fastesteditExisting
which will edit existing same constraints relation between views created by NamadaLayout, and added constraints if it's new. Since it's literraly will check the same constraint's relation, it will be slightly slower thanaddNew
removeOldAndAddNew
which will remove all current constraints created by NamadaLayout and added new constraints. Since it's literraly will check all the constraint's identifier, it will be slightly slower thanaddNew
cleanLayoutAndAddNew
which will remove all current constraints created by NamadaLayout and will remove all subviews from it's parent. Since it's literraly will check all the constraint's identifier and loop all subviews and remove it from superview, it will be the slowest method but the safest.
we will talk about the delegate later.
example:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent(.editExisting) { content in
content.put(someView)
}
}
}
The above code will add someView
into ViewController view child and edit existing constraint in ViewController view and its child if have any
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent(.removeOldAndAddNew) { content in
content.put(someView)
.layoutContent { someViewContent in
someViewContent.put(someOtherview)
}
}
}
}
The above code will add someView
into ViewController view child, and then put someOtherView
into someView
child and remove existing constraint in ViewController view and its child if have any
If you have stackView, you can put your view stacked inside or not too:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someStack)
.layoutContent { someStackContent in
someStackContent.putStacked(stackedView)
someStackContent.putSpace(by: 8)
someStackContent.putStacked(otherStacked)
someStackContent.put(someOtherview)
}
}
}
}
The above code will add someStack
into ViewController view child, and then put stackedView
, space by 8 point and otherStacked
into stackedView
arranged child and put someOtherview
inside someStack
but not stacked.
You can embed UIViewController
too:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someViewController)
}
}
}
There are some method to create and add specific view which I believe cover almost if not all, default view from apple UIKit:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putView()
content.putView(assignedTo: &myViewVariable)
content.putLabel()
content.putLabel(assignedTo: &myLabelVariable)
}
}
}
as you can see in the code, it will create view and automatically assign it to your variable if you provide any. This kind of method is available too for stack:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putStack().layoutContent { stackContent in
stackContent.putStackedView()
stackContent.putStackedView(assignedTo: &myViewVariable)
stackContent.putStackedTextView()
stackContent.putStackedTextView(assignedTo: &myTextViewVariable)
}
}
}
}
Position Constraint
All layout have edges and you can create very readable constraints like this:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: topView.bottomAnchor)
.left(.moreThan, to: leftView.rightAnchor)
.right(.lessThan, to: rightView.leftAnchor)
.bottom(.equalTo(18), to: bottomView.topAnchor)
.centerY(.moreThanTo(9), to: centerView.centerYAnchor)
.centerX(.lessThanTo(4.5), to: centerView.centerXAnchor)
}
}
}
The above code is adding someView
into ViewController view childs and then creating this constraints:
someView
top is equal totopView
bottomsomeView
left is greaterThanOrEqual toleftView
rightsomeView
right is lessThanOrEqual torightView
leftsomeView
bottom equal to distance tobottomView
top by 18someView
centerY greaterThanOrEqual to distance tobottomView
centerY by 9someView
centerX lessThanOrEqual to distance tobottomView
centerX by 4.5
If you want to make View Constraints with its parent then just pass .parent
or .safeArea
instead:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: .safeArea)
.left(.moreThan, to: .parent)
.right(.lessThan, to: parent)
.bottom(.equalTo(18), to: safeArea)
}
}
}
The above code is adding someView
into ViewController view childs and then creating this constraints:
someView
top is equal to its parent's topsomeView
left is greaterThanOrEqual to its parent's leftsomeView
right is lessThanOrEqual to its parent's rightsomeView
bottom equal to distance to its parent's bottom
If you don't have any variable that pointing to the view you want to constrained, just use AnonymousRelation
:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putView()
content.putView()
.top(.equal, to: Anchor(of: .previous).bottomAnchor)
.left(.equal, to: Anchor(of: .parent).leftAnchor)
}
}
}
The above code is adding two anonymous View
into ViewController view childs and then creating this constraints:
first view
top is equal toprevious view
bottom anchorfirst view
left is equal toparent view
left anchor
there are 6 types of AnonymousRelation
:
parent
which is same as give relation to superviewsafeArea
which is same as give relation to superview safeareamyself
which is same as give relation to itselfmySafeArea
which is same as give relation to itself safeareaprevious
which is same as give relation to previous layouted viewpreviousSafeArea
which is same as give relation to previous layouted view safearea
There are some shortcut to create multiple constraint at once like:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.center(.equal, to: otherView)
.vertical(.moreThanTo(18), to: .safeArea)
.horizontal(.lessThanTo(UIHorizontalInsets(left: 9, right: 18)), to: .parent)
content.put(tableView)
.edges(.equalTo(18), to: .parent)
content.put(imageView)
.inBetween(of: someView, and: otherView, .horizontally(.equal))
content.put(logoView)
.at(.topLeft, equalTo(9), to: .safeArea)
.at(.topOf(otherView), .equal)
}
}
}
center
is shortcut to setcenterX
andcenterY
to other view simultaniouslyvertical
is shortcut to settop
andbottom
to parent or safeArea simultaniouslyhorizontal
is shortcut to settop
andbottom
to parent or safeArea simultaniouslyedges
is shortcut to settop
,bottom
,left
andright
to parent or safeArea simultaniouslyinBetween
is shortcut to settop
andbottom
orleft
andright
to two view simultaniouslyat
is shortcut to settop
,bottom
,left
orright
to parent or safeArea simultaniouslyat
can be other shortcut too to settop
,bottom
,left
,right
and center to other view simultaniously
Dimension Constraints
To define dimension relation with other you can just do something like this:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.width(.equalTo(.parent), multiplyBy: 0.75, constant: 18)
.height(.lessThanTo(90))
.height(moreThanTo(otherView.heightAnchor))
}
}
}
The above code is adding someView
into ViewController view childs and then creating this constraints:
someView
width is equal to parent, multiplied by 0.75, added 18.someView
height is lessThanOrEqual to 90someView
height is greaterThanOrEqual to its otherView's height
you can use AnonymousRelation
if you need too:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.width(.equalTo(.parent), multiplyBy: 0.75, constant: 18)
.height(.equalTo(Anchor(of: .parent).widhtAnchor))
}
}
}
The above code is adding someView
into ViewController view childs and then creating this constraints:
someView
width is equal to parent, multiplied by 0.75, added 18.someView
height is equal to its parent's width
There are some shortcuts too:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.size(.lessThan(otherView), multiplyBy: 0.75, constant: 18)
.size(.moreThan(CGSize(widht: 90, height: 90)))
}
}
}
which can be described by the code above
Scroll Automatic Content Constraints
you can add content view for UIScrollView which have constraints to make sure the content will automatically resized according to the content by using putScrollVContentView()
like this:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putScrollVContentView()
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
or if you already have custom content view just use putAsScrollVContent<View: UIView>(_ view: View)
:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putAsScrollVContent(myScrollContentView)
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
and for horizontal content just use putScrollHContentView()
:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putScrollHContentView()
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
or if you already have custom content view just use putAsScrollHContent<View: UIView>(_ view: View)
:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putScroll()
.edges(.equal, to: .parent)
.layoutContent { scroll in
scroll.putAsScrollHContent(myScrollContentView)
.layoutContent { scrollContent in
// do layout vertical scroll content
...
...
}
}
}
}
}
Priority
To define constraint priority, just pass UILayoutPriority
at any method you want. If you don't pass priority, it will assign the priority using simple rules which is the first constraint will be have more priority than the second, and so on. The start default priority is 999
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.put(someView)
.top(.equal, to: .safeArea, priority: .required)
.left(.moreThan, to: .parent, priority: .defaultHigh)
.right(.lessThan, to: parent, priority: .defaultLow)
.bottom(.equalTo(18), to: safeArea, priority: 1000)
}
}
}
Build during layout
If you want to setup the view during layout, just call the property like a setter method:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putTextView()
.backgroundColor(.white)
.text("some text")
.edges(.equal, to: .safeArea)
}
}
}
or by using apply:
class ViewController: UIViewController {
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutContent { content in
content.putTextView()
.edges(.equal, to: .safeArea)
.apply {
$0.backgroundColor = .white
$0.text = "some text"
}
}
}
}
or even both
Delegate
There's a delegate which can passed when you create layout:
public protocol NamadaLayoutDelegate: class {
func namadaLayout(viewHaveNoSuperview view: UIView) -> UIView?
func namadaLayout(neededViewControllerFor viewController: UIViewController) -> UIViewController?
func namadaLayout(_ view: UIView, erroWhenLayout error: NamadaError)
}
All the method are optional since all the default implementation are already defined in the extensions. The purpose of each methods are:
namadaLayout(viewHaveNoSuperview view: UIView) -> UIView?
will be called when you call relation with parent, but your layout is have no parent (like in top view in UIViewController). The default implementation will be throw LayoutErrorfunc namadaLayout(neededViewControllerFor viewController: UIViewController) -> UIViewController?
will be called when you embed UIViewController, but your layout have no UIViewController (like in UITableViewCell). The default implementation will be throw LayoutErrorfunc namadaLayout(_ view: UIView, erroWhenLayout error: NamadaError)
will be called when you there's any LayoutError when creating constraints.
You can pass the delegate when first call layoutContent or layout like this this:
layoutContent(withDelegate: yourDelegate) { content in
...
...
}
Molecule
You can create molecule using MoleculeView protocol:
class MoleculeView: UIView, MoleculeView {
...
...
...
func layoutContent(_ layout: LayoutInsertable) {
layout.put(someView)
...
...
layout.put(someOtherView)
...
...
}
func moleculeWillLayout() {
//will run before layouting
...
...
}
func moleculeDidLayout() {
//will run after layouting
...
...
}
}
The layoutContent will be called if the MoleculeView
is added to superView using layout
, or layoutContent
MoleculeView have optional func which are:
moleculeWillLayout()
which will run before layoutingmoleculeDidLayout()
which will run after layouting
Cell Molecule
You can create Cell (UITableViewCell or UICollectionViewCell) Molecule. Just extend CollectionMoleculeCell
for UICollectionViewCell or TableMoleculeCell
for UITableViewCell.
class MyTableCell: TableCellLayoutable {
...
...
override var layoutBehaviour: CellLayoutBehaviour { .layoutOn(.reused) }
override func layoutContent(_ thisLayout: layout) {
layout.put(someView)
...
...
layout.put(someOtherView)
...
...
}
}
LayoutChild will run when the cell first layout, or depends on CellLayoutBehaviour if you override it. The behaviour are:
layoutOnce
wich will ensure layoutChild only run once on the first layoutlayoutOn(CellLayoutingPhase)
wich will layout on first layout and on phaselayoutOnEach([CellLayoutingPhase])
wich will layout on first layout and on each phassesalwaysLayout
wich will layout on every phase
the phase are:
firstLoad
setNeedsLayout
reused
none
you can implement func layoutOption(on phase: CellLayoutingPhase) -> SublayoutingOption
to tell which SubLayoutingOption you want for every phase. the default is .addNew
on firstLoad and .removeOldAndAddNew
for the rest
class MoleculeCell: TableCellLayoutable {
...
...
func layoutOption(on phase: CellLayoutingPhase) -> SublayoutingOption {
return .removeOldAndAddNew
}
}
If your UITableView
or UICollectionView
have custom calculated size, you can just override calculatedCellSize(for collectionContentWidth: CGFloat) -> CGSize
for UICollectionViewCell and calculatedCellHeight(for cellWidth: CGFloat) -> CGFloat
for UITableViewCell.
class MyCollectionCell: CollectionCellLayoutable {
...
...
//default return value is CGSize.automatic
override func calculatedCellSize(for collectionContentWidth: CGFloat) -> CGSize {
let side: CGFloat = collectionContentWidth / 3
return .init(width: side, height: side)
}
...
...
}
or for UITableViewCell
class MyTableCell: TableCellLayoutable {
...
...
//default return value is CGFloat.automatic
override func calculatedCellHeight(for cellWidth: CGFloat) -> CGFloat {
cellWidth / 3
}
...
...
}
Binding
To do binding, you need to create property with same type as View property you want to bind and add @ViewState
attributes.
Keep in mind that the only property can be binded is the one native with UIKit and this library
@ViewState searchPhrase: String?
Two way binding
Two way binding is binding when the property is changing it will apply the changes into View binded or vice versa. It will not work on read only View property.
Then you can bind it manually like this using projectedValue and keyPath:
$searchPhrase.bind(with: yourSearchBarToBind, \.text)
// or
$searchPhrase.map(from: yourSearchBarToBind, \.text)
// or
$searchPhrase.apply(into: yourSearchBarToBind, \.text)
the only difference between those methods are:
bind
which will only bind view with propertymap
which will bind and get the current value of the view property into binded propertyapply
which will bind and apply the current value of the binded property into view property
and you can add binding observer:
$searchPhrase.bind(with: yourSearchBarToBind, \.text)
.viewDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when view changes
}
).stateDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when state changes
}
)
The viewDidSet will run when view property is changing. The stateDidSet will run when property binded is changing
Or you can bind when layouting:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.bind(\.text, with: $typedText)
or
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.apply(\.text, from: $typedText)
or
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.map(\.text, into: $typedText)
One way binding
One way binding is binding when the binded property will read changes from View propertry binded, But will not apply changes into View property when the binded property change.
To do one way binding, you need to create property with same type as View property you want to bind and add @ViewState
attributes
@ViewState searchPhrase: String?
Then you can bind it manually like this using projectedValue and keyPath:
$searchPhrase.oneWayBind(with: yourSearchBarToBind, \.text)
and you can add binding observer:
$searchPhrase.oneWayBind(with: yourSearchBarToBind, \.text)
.viewDidSet(then: { searchBar, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
// do something when view changes
}
)
keep in mind, state didSet will not applicable when doing one way binding.
Or you can bind when layouting:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.oneWayBind(\.text, with: $typedText)
Observing state
If you want to observe the changes of states you can use @ViewState
or @ObservableState
. The difference between two is:
- ObservableState cannot be binded to UIView property, so it will be good if you just want to add didSet or willSet at the property ignoring what in the View
- ViewState can be observed and binded to UIView property. But if you want to do both, keep in mind that the Type of property should be same as the View and it will changes automatically according to the View state, so don't do assignment to the same binded property because it potentially create a stack overflow
@ViewState searchPhrase: String?
or if you want to use ObseravbleState:
@ObservableState searchPhrase: String?
Then:
$searhPhrase.observe(observer: self).willSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is will change
}.didSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is change
}
There is three trigger enumeration:
view(UIView)
which means the closure is triggered from changes in Viewstate
which means the closure is triggered from changes directly into binded propertybind
which means the closure is triggered when in binding process
You can delay didSet closure run like this:
$searhPhrase.observe(observer: self)
.delayMultipleSetTrigger(by: 1)
.didSet { selfObserver, changes in
let newValue = changes.newValue
let oldValue = changes.oldValue
let trigger = changes.trigger
let viewThatTriggerChanges: UIView? = trigger.triggeringView
// do something when searchPhrase is change
}
Which means when multiple set is triggered with interval under one second, it will wait until one second to run next closure with latest changes, and ignore any changes occures in those interval execpt the last one which will scheduled to run in the next closure run. Keep in mind that the willSet will not affected with delay.
You can always observe get too:
$searhPhrase.observe(observer: self).willGet { selfObserver, value in
// do something when searchPhrase property will get
}.didGet { selfObserver, value in
// do something when searchPhrase property did get
}
Or you can observe when layouting with same type property:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.whenStateChanged(for: $someText, thenAssignTo: \.text)
or with autoclosure which can be cross type property:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.whenStateChanged(for: $someText, thenAssign: \.text, with: "assigned with \(someText)")
or with closure observer:
layoutContent { content in
content.put(searchBar)
.at(.fullTop, .equal, to: .safeArea)
.observe($someText) { searchBar, changes in
searchBar.text = changes.new
}
View Model
Basic View Model
You can create View Model by extending ViewModel class:
class MyViewModel: ViewModel<MyView> {
@ViewState image: UIImage?
@ViewState text: String?
override func willApplying(_ view: MyView) {
// do something when view model will applying view
}
override func didApplying(_ view: MyView) {
// do something when view model did applying view
}
override func modelWillMapped(from view: MyView) {
// do something when view model will mapped view
}
override func modelDidMapped(from view: MyView) {
// do something when view model did mapped view
}
override func willUnbind() {
// do something when view model will unbind
}
override func didUnbind() {
// do something when view model did unbind
}
override func bind(with view: MyView) {
super.bind(with: view)
$image.bind(with: view.imageView, \.image)
$text.bind(with: view.textView, \.text)
$text.observe(observer: self)
.delayMultipleSetTrigger(by: 1)
.didSet { model, changes in
// do something
}
}
}
ViewModel generic parameter can be anything that extend NSObject, like UIView, or UIViewController. Then you can use the View model like this:
class MyView: UIViewController {
override func viewDidLoad() {
super .viewDidLoad()
layoutView()
let viewModel: MyViewModel = .init()
viewModel.apply(into: self)
}
}
you can apply, map or just bind your view to ViewModel just like usual binding. It will automatically set all the @ViewState
attributes to run those behaviour when you do binding.
Cell View Model
To create View Model for cell which will support reusability of cell, you can use CollectionViewCellModel<Cell: UICollectionViewCell>
for Collection and TableViewCellModel<Cell: UITableViewCell>
for Table. The rest is same like ViewModel, except the generic parameter.
class MyCellView: CollectionMoleculeCell {
...
...
}
class MyCellViewModel: CollectionViewCellModel<MyCellView> {
@ViewState image: UIImage?
@ViewState text: String?
override func willApplying(_ view: MyCellView) {
// do something when view model will applying view
}
override func didApplying(_ view: MyCellView) {
// do something when view model did applying view
}
override func modelWillMapped(from view: MyCellView) {
// do something when view model will mapped view
}
override func modelDidMapped(from view: MyCellView) {
// do something when view model did mapped view
}
override func willUnbind() {
// do something when view model will unbind
}
override func didUnbind() {
// do something when view model did unbind
}
override func bind(with view: MyCellView) {
super.bind(with: view)
$image.bind(with: view.image, \.image)
$text.bind(with: view.label, \.text)
}
}
To apply the cell into UITableView
or UICollectionView
you just need to set the model into cells property in UITableView
, or UICollectionView
let cellModels: [CollectionCellModel] = items.compactMap { item in
let cellModel: MyCellViewModel = build {
$0.cellIdentifier = item.itemId
$0.image = item.image
$0.text = item.itemName
}
return cellModel
}
table.cells = cellModels
It will automatically reload existing cells with new cells and only reload cell with different cellIdentifier. Cell identifier can be anything as long as it's Hashable
If your table pr collection is sectionable, you can create section with cells and assign it to table sections:
let firstSection: UICollectionView.Section = .init(
identifier: "first_section",
cells: items.compactMap { item in
let cellModel: MyCellViewModel = build {
$0.cellIdentifier = item.itemId
$0.image = item.image
$0.text = item.itemName
}
return cellModel
}
)
let secondSection: UICollectionView.Section = .init(
identifier: "second_section",
cells: users.compactMap { user in
let cellModel: MyOtherCellViewModel = build {
$0.cellIdentifier = user.userId
$0.image = user.image
$0.text = user.itemName
}
return cellModel
}
)
table.sections = [firstSection, secondSection]
Section identifier can be anything as long as it's Hashable
There are some default section you can use, which is
UITableView.Section
andUICollectionView.Section
default plain sectionUITableView.TitledSection
andUICollectionView.TitledSection
which is section with titleUICollectionView.SupplementedSection
which is UICollectionView Section with custom header or/and footer
If you want to directly get default binded model with UICollectionView or UITableView, just get it from model property. The cells, and section property are actually the property of the UICollectionView or UITableView ViewModel.
let tableModel: UITableView.Model = table.model
let collectionModel: UICollectionView.Model = table.model
CellBuilder
Creating Sectioned Cell to UITableView
or UICollectionView
can be easier if you are using TableCellBuilder
or CollectionCellBuilder
:
table.sections = TableCellBuilder(sectionId: "first_section").
next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.next(type: AnyCell.self, from: myItems) { cell, item in
// do apply item into cell
// this closure is escaping and will run when cell is first created or for every reused
}.nextEmptyCell(with: cellHeight) { cell in
cell.contentView.backgroundColor = .gray
// do any setup to empty cell here
// this closure is escaping and will run when cell is first created or for every reused
}.nextSection(sectionId: "second_section")
.next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.build()
you can append existing cell using this too:
collection.sections = collection.sections.append()
next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.next(type: AnyCell.self, from: myItems) { cell, item in
// do apply item into cell
// this closure is escaping and will run when cell is first created or for every reused
}.nextEmptyCell(with: cellSize) { cell in
cell.contentView.backgroundColor = .gray
// do any setup to empty cell here
// this closure is escaping and will run when cell is first created or for every reused
}.nextSection(sectionId: "second_section")
.next(type: MyCellModel.self, from: myItems) { cellVM, item in
// do apply item into cell view model
}.build()
ObservableView
If you have any view that you want to observe by ViewModel
by delegate, you can just implement ObservableView
and provide Observer
. It will have a variable named observer
, which is current binded ViewModel and casting it to Observer
type. So don't forget to implement the ObserverType to your ViewModel. It's better to make the Observer extend ViewModelObserver
since it have method to notify ViewModel that it finished Layouting and then ViewModel will automatically apply View with ViewModel if the type match:
protocol MyScreenObserver: ViewModelObserver {
func myScreen(_ screen: MyScreen, didPullToRefresh refreshControl: UIRefreshControl)
}
class MyScreen: UIViewController, ObservableView {
typealias Observer = MyScreenObserver
...
...
...
override func viewDidLoad() {
super.viewDidLoad()
layoutView()
// will apply MyScreen with any binded ViewModel if already binded
// ViewModel class are implement ViewModelObserver
observer?.viewDidLayouted(self)
}
@objc func didPullToRefresh() {
observer?.myScreen(self, didPullToRefresh: refreshControl)
}
}
Extras
NamadaLayout have some extra feature which is:
View Builder
All View are implement Buildable
protocol which make the View can be instatiable with build function like this:
lazy var button: UIButton = build {
$0.setTitle("My Button", for: .normal)
$0.setTitleColor(.black, for: .normal)
}
or like this, which using dynamicMemberLookup
which make sure you can call any property name as function assignment:
lazy var topLabel = build(UILabel.self)
.text("any text")
.textColor(.black)
.textAlignment(.center)
.build()
or if your object is not Buildable
, just instantiate your view into build function:
lazy var topLabel = build(MyObject())
.text("any text")
.build()
TextCompatible
UILabel, UITextField and UITextView can be assigned using TextCompatible which is implemented on NSAttributedString or String.
label.textCompat = "some text"
texView.textCompat = someAttributedString
textField.placeholderCompat = "some"
ImageCompatible
UIImageView can be assigned using ImageCompatible whic is implemented on UIImage and UIImageView.Animation.
imageView.imageCompat = someImage
imageAnimated.imageCompat = UIImageView.Animation(images: seriesOfImages, duration: 1, animating: true, repeatCount: Int = 2)
UIButton Click Observer
UIButton have didClicked function to assign closure to observe click event in UIButton.
button.didClicked(then: { _ in
// do something
})
UICollectionView and UITableView Reload Observer
UICollectionView and UITableView model have didReload function to assign closure to listen when reload is finished.
collectionView.model.didReload(then: { _ in
// do something
})
Contribute
You know how, just clone and do pull request