Writing Layouts
Format
EverLayout uses a collection of parser protocols to retrieve the pertinent layout information from your layout files. Theoretically, layouts can be written in any format as long as you can provide working parsers.
The following documentation demonstrates how to use EverLyout to write layouts for the default JSON parsers, however if you're interested in writing layouts using a different format or general structure, read Layout Formats to see how to write your own parsers.
The Layout Index
The layout index is the root of every layout model.
{
"name":"SimpleLayout",
"root":{...},
"templates":{...},
"controllerTitle":"My App!",
"navigationBar":{...}
}
5 properties are currently read in the Layout Index:
name
The layout name. So far this is only used to route layout updates when developing with EverLayout Bridge.root
Layouts are built on UIViews, which become the 'root view' of the layout. This will most typically be your View Controller's view.templates
EverLayout supports templates which are reusable throughout the entire layout. Think of these as super-classes for your view models.controllerTitle
If your layout is built on a view contained in aUINavigationController
, this property will set the title of theUINavigationBar
.navigationBar
Responsible for setting otherUINavigationBar
properties.
The View Model
The view model contains layout data for individual UIViews.
{
"template":"exampleTemplate",
"constraints":{...},
"properties":{...},
"z-index":"2",
"views":{...}
}
template
See Templatesconstraints
See Writing Constraintsproperties
See View Propertiesz-index
See Z-Indexviews
See Subviews
View Name
Every view in the layout (except the root view, see View Index) has a name and is expressed as a subview of another view in this layout.
{
...
"views":{
"actionButton":{
// View model data
}
}
}
The key in the key-value pair is the view's name, and the value is its view model.
In the sample, EverLayout would scan this layout's view environment for a property with the name 'actionButton'. If it finds one, it will map this view model to that UIView, including adding it as a subview.
If EverLayout is unable to find the view, it will attempt to find the property in the environment's super-classes, before moving on.
Creating New Views
The default behaviour for EverLayout is to find the view being referenced by scanning the properties of its view environment. However in some cases you may wish to create new views from inside the layout file and have them become a part of your view hierarchy.
This means:
- You can create basic views to test layout ideas without having to re-compile (when using EverLayout Bridge).
- Views with basic functionality (like wrappers for other views) no longer need to be described in your code.
- More sophisticated layout updates can be made without having to update your code and re-submit your app.
If your view name is prefixed with !
, it will be considered a
'new view' and created during the build process.
{
"!newWrapperView":{
// View model data
}
}
Creating New Views From UIView Subclasses
In addition to being able to create new views, you can also specify which UIView subclass you want your new view to be.
{
"!newImageView:UIImageView": {
...
}
}
Append the name of your new view with :
followed by
a class name. In the example EverLayout creates a new
UIImageView.
To create an instance of a custom class, you must also specify your application namespace, like so:
!myNewClassInstance:MyApp.MyClassName
Writing Constraints
EverLayout uses AutoLayout constraints to position its elements. The constraints for each view are generated during the layout build based on key-value rules in the view model.
The constraint arguments can be written in comprehensive and shorthand formats.
Shorthand constraints are written as key-value pairs; The key denotes which attributes to constrain, and the value is a representation of the layout arguments to apply to each of these attributes. The syntax for shorthand constraints may be difficult to read at first, but after becoming familiar with this approach, layout design will be a much quicker process.
Comprehensive
{
"constraints":{
"top right bottom left":{
"to":"super"
}
}
}
Shorhand
{
"constraints":{
"top right bottom left":"@super"
}
}
The example above creates 4 NSLayoutConstraints that attach the left, top, right and bottom attributes of this view to their counterparts in the superview.
Comprehensive
{
"constraints":{
"bottom left right":{
"to":"super",
"inset":8
},
"height":{
"to":"super",
"multiplier":0.5
}
}
}
Shorthand
{
"constraints":{
"bottom left right":"@super <8",
"height":"@super /2"
}
}
The second example creates constraints which tether the bottom, left and right edges to the superview with an 8 unit inset, and then gives this view a height of half the superview height.
In the attribute declarations (the key), and the shorthand arguments (the value), each argument is space separated.
For the left-hand statement, any NSLayoutAttribute is valid along with some additional 'compound attributes'.
edges
Translates totop
left
right
bottom
center
Translates tocenterX
centerY
margins
Translates tomarginLeft
marginRight
The right-hand statement can have many arguments, each denoted by a modifier character in shorthand.
to
/@
Target a view in the layout / view environmentattribute
/.
A dot separator can be added to a target name to specify an NSLayoutAttribute of that view to target (e.g@viewName.right
)constant
/+
/-
A constant (positive or negative)inset
/<
An inset constant, which is basically a constant where+
and-
are inferred from contextmultiplier
/*
//
A multiplier or convenience divideridentifier
/!
Constraint identifier (for debugging)relation
/%
A relation (e.g%>=
creates a 'greater than or equal to' relation). Comprehensive values aregte
andlte
.priority
/$
A constraint priorityidentifier
/!
Give the constraints an identifier to more easily identify them in the console
Note: EverLayout will try to infer missing constraint properties based on the information that is supplied.
"width": "@view +12"
Will create a constraint with the width
of view
, plus 12 units.
"width": "+12"
Will create a constraint that has a width of
12 units.
Multiple constraints
Since in JSON structures a key is unique, each layout attribute can only have one constraint argument, which will cause problems if you wish to have multiple constraints with different priorities or relations. To get around this the constraint arguments can be an array of the comprehensive and shorthand arguments described above.
{
"constraints":{
"width":[
{
"constant":100,
"priority":500
},
{
"constant":150,
"priority":700
}
]
}
}
Size Classes
A single layout file can support multiple screen sizes/orientations by allowing the activation and deactivation of layout constraints dependant on size classes.
Size classes are applied as regular constraint properties as demonstrated above but will prevent the activation of the constraint unless the view we're working with matches the specified size classes.
Like the other properties, the size classes have both comprehensive and shorthand syntax.
horizontalSizeClass / h
verticalSizeClass / c
with value options:
regular / r
compact / c
{
"constraints":{
"width height":[
"+50 hc vc",
"+100 hr vc"
]
}
}
{
"constraints":{
"width height":{
[
"constant":50,
"horizontalSizeClass":"compact",
"verticalSizeClass":"compact"
],
[
"constant":100,
"horizontalSizeClass":"compact",
"verticalSizeClass":"regular"
]
}
}
}
The examples above will constrain the view to a width and height of 50 points in a horizontal iPhone, and the same view to a width and height of 100 points in a vertical iPhone.
Updating trait collection at runtime
If the case of having different constraints for device orientations, you'll want to be able to update
the layout at runtime when the trait collection updates. You can do this by overriding
willTransitionToNewCollection
.
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
self.layout.updateConstraints(withTraitColelction: newCollection)
}
View Properties
Some properties of UIView (and its subclasses) have been made settable
in the layout files. You can set these under properties
in the view model.
These are the properties currently available:
UIView
frame
Value is passed into CGRectFromStringbackgroundColor
Color can be the name of the color (red
) or a hex string (#333333
).hidden
(isHidden)cornerRadius
Translates tolayer.cornerRadius
borderWidth
Translates tolayer.borderWidth
borderColor
Translates tolayer.borderColor
alpha
clipToBounds
contentMode
hidden
UILabel
text
textColor
lineBreakMode
textAlignment
numberOfLines
fontSize
UIButton
text
Translates to a call to setTitle(...)fontSize
textColor
Translates to a call to setTitleColor(...)backgroundImage
Translates to a call to setBackgroundImage(...)image
Translates to a call to setImage(...)
UIImageView
image
The value used is passed into UIImage(named: ...). If there is no local asset found, EverLayout will check if the value is a URL and attempt to load a remote image (this is helpful for development, but probably not practical for production).
UIScrollView
contentInset
Value is passed into UIEdgeInsetFromStringcontentOffset
Value is passed in CGPointFromString
UITextField
placeholder
Navigation Bar Properties
If your root view is contained within a UINavigationController
, you can set properties
for the UINavigationBar within your view index, like so:
{
"name":"layout-name",
"root":{
...
},
"navigaitonBar":{
// Properties
}
}
These are the available properties:
textColor
translucent
backgroundColor
tintColor
Z-Index
The order in which EverLayout adds subviews is not always clear. If you
have views which overlap each other, the z-index
can be set
to tell EverLayout which views should be at the top.
{
"onBottom": {
"z-index":"1"
},
"onTop":{
"z-index":"2"
}
}
Like in CSS, the higher the z-index
the closer to the surface the view
will be.
Subviews
Every view in the layout (except the root view) is added as a subview of another. Subviews are added like so:
{
"constraints": {...},
"properties":{...},
"views":{
"firstSubview":{
...
},
"secondSubview": {
...
}
}
}
The key is the view's name (see view names), and the value is the view model (see view model).
Templates
A layout can have templates to be reused.
{
"name":"LayoutExample",
"root":{
"!buttonOne:UIButton":{
"template":"button",
"properties":{
...
},
"constraints":{
...
}
},
"!button2:UIButton":{
"template":"button",
"properties":{
...
},
"constraints":{
...
}
}
},
"templates":{
"button":{
"properties":{
"backgroundColor":"#333333"
},
"constraints":{
"width":"+200",
"height":"+40"
}
}
}
}
In the example the layout is creating two new buttons that are both inheriting the same properties
from the template button
.
Templates currently support properties
and constraints
.
A view can inherit from multiple templates by passing the names into an array.
{
"!myView":{
"template":["firstTemplate" , "secondTemplate"]
}
}
Data Injection
'Data Injection' is a glorified 'Find and Replace' on your source layout
data. You can mark 'variables' in your layout files like so: #{propertyName}
{
"viewName":{
"properties":{
"backgroundColor":"#{color}"
}
}
}
Before building your layout:
self.layout?.injectData([
"color":"red"
])
EverLayout will replace all instances of #{color}
in your layout
with red
.