Stevia π - Human readable Auto Layout
Reason - Example - Live Reload - Installation - Documentation
layout(
100,
|-email-| ~ 80,
8,
|-password-| ~ 80,
"",
|login| ~ 80,
0
)Reason
Why
Because nothing holds more truth than pure code
Xibs and storyboards are heavy, hard to maintain, hard to merge.
They split the view concept into 2 separate files making debugging a nightmare
There must be a better way
How
By creating a tool that makes Auto layout code finally readable by a human being.
By coupling it with live code injection such as injectionForXcode we can design views in real time
View layout becomes fun, concise, maintainable and dare I say, beautiful
What
- Auto Layout DSL
- Pure Swift
- Simple, this is just NSLayoutConstraint shortcuts, pure UIKit code, no voodoo magic
- Chainable api
Advantages of Stevia
- No more constraints hell in Interface builder.
- No more debugging in Interface builder toggling checkboxes.
- The view code is not split between 2 files anymore
- What you see is what you get, your view code is in one place, there is no hidden logic elsewere (in the xib)
- No more referencing Storyboards or Xibs by their names "ProfileStoryboard". We all know strings are bad identifiers.
- Clear view Hierarchy
- Live reload, WHAT YOU SEE IS WHAT YOU GET
- Events are a breeze
- Code views Faster
- No more XML (Thank God!)
- Better readability 1000lines XML file vs. 30lines code
- Readable constraints (they actually look like the layout itself \o/)
- Horizontal & vertical layout can be described at the same time
- Styles are well separated, concise, reusable and can be composed
- Content like text, placeholders are easier to visualize
Login View Example
Before (Native Autolayout)
class LoginViewNative:UIView {
let email = UITextField()
let password = UITextField()
let login = UIButton()
convenience init() {
self.init(frame:CGRectZero)
backgroundColor = .whiteColor()
addSubview(email)
addSubview(password)
addSubview(login)
email.translatesAutoresizingMaskIntoConstraints = false
password.translatesAutoresizingMaskIntoConstraints = false
login.translatesAutoresizingMaskIntoConstraints = false
addConstraint(NSLayoutConstraint(
item: email,
attribute: .Left,
relatedBy: .Equal,
toItem: self,
attribute: .Left,
multiplier: 1,
constant: 8)
)
addConstraint(NSLayoutConstraint(
item: email,
attribute: .Right,
relatedBy: .Equal,
toItem: self,
attribute: .Right,
multiplier: 1,
constant: -8)
)
addConstraint(NSLayoutConstraint(
item: password,
attribute: .Left,
relatedBy: .Equal,
toItem: self,
attribute: .Left,
multiplier: 1,
constant: 8)
)
addConstraint(NSLayoutConstraint(
item: password,
attribute: .Right,
relatedBy: .Equal,
toItem: self,
attribute: .Right,
multiplier: 1,
constant: -8)
)
addConstraint(NSLayoutConstraint(
item: login,
attribute: .Left,
relatedBy: .Equal,
toItem: self,
attribute: .Left,
multiplier: 1,
constant: 0)
)
addConstraint(NSLayoutConstraint(
item: login,
attribute: .Right,
relatedBy: .Equal,
toItem: self,
attribute: .Right,
multiplier: 1,
constant: 0)
)
for b in [email, password, login] {
addConstraint(NSLayoutConstraint(
item: b,
attribute: .Height,
relatedBy: .Equal,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 1,
constant: 80)
)
}
addConstraint(NSLayoutConstraint(
item: email,
attribute: .Top,
relatedBy: .Equal,
toItem: self,
attribute: .Top,
multiplier: 1,
constant: 100)
)
addConstraint(NSLayoutConstraint(
item:email,
attribute: .Bottom,
relatedBy: .Equal,
toItem: password,
attribute: .Top,
multiplier: 1,
constant: -8)
)
addConstraint(NSLayoutConstraint(
item: login,
attribute: .Bottom,
relatedBy: .Equal,
toItem: self,
attribute: .Bottom,
multiplier: 1,
constant: 0)
)
email.placeholder = "Email"
email.borderStyle = .RoundedRect
email.autocorrectionType = .No
email.keyboardType = .EmailAddress
email.font = UIFont(name: "HelveticaNeue-Light", size: 26)
email.returnKeyType = .Next
password.placeholder = "Password"
password.borderStyle = .RoundedRect
password.font = UIFont(name: "HelveticaNeue-Light", size: 26)
password.secureTextEntry = true
password.returnKeyType = .Done
login.setTitle("Login", forState: .Normal)
login.backgroundColor = .lightGrayColor()
login.addTarget(self, action: "loginTapped", forControlEvents: .TouchUpInside)
login.setTitle(NSLocalizedString("Login", comment: ""), forState: .Normal)
}
func loginTapped() {
//Do something
}
}With Stevia π
class LoginViewStevia:UIView {
let email = UITextField()
let password = UITextField()
let login = UIButton()
convenience init() {
self.init(frame:CGRectZero)
backgroundColor = .whiteColor()
sv(
email.placeholder("Email").style(fieldStyle), //.style(emailFieldStyle),
password.placeholder("Password").style(fieldStyle).style(passwordFieldStyle),
login.text("Login").style(buttonStyle).tap(loginTapped)
)
layout(
100,
|-email-| ~ 80,
8,
|-password-| ~ 80,
"",
|login| ~ 80,
0
)
}
func fieldStyle(f:UITextField) {
f.borderStyle = .RoundedRect
f.font = UIFont(name: "HelveticaNeue-Light", size: 26)
f.returnKeyType = .Next
}
func passwordFieldStyle(f:UITextField) {
f.secureTextEntry = true
f.returnKeyType = .Done
}
func buttonStyle(b:UIButton) {
b.backgroundColor = .lightGrayColor()
}
func loginTapped() {
//Do something
}
}Number of lines From 144 -> 57 ( ~ divided by 2.5)
Number of characters From 4231 -> 1338 ( ~ divided by 3)
Write 3 times less code that is actually 10X more expressive and maintainable <3
Live Reload
You can even enable LiveReload during your development phase!
Stevia + InjectionForXcode = <3 (WhoNeedsReactNative??)
-
Download InjectionForXcode
-
Install it, Launch it and Go to
File>Install Plugins(cmd+i) -
Restart Xcode and make sure to click
Load bundleson the popup
In order to support live reload with InjectionForXcode, we simply need to tell our ViewController to rebuild a view after an injection occured.
in viewDidLoad() add :
on("INJECTION_BUNDLE_NOTIFICATION") {
self.view = MyView()
}Currently InjectionForXcode doesn't seem to swizzle init methods for some reason. So we have to move our view code in another methods
convenience init() {
self.init(frame:CGRectZero)
//View code
}Becomes
convenience init() {
self.init(frame:CGRectZero)
render()
}
func render() {
//View code
}
And Voila :)
Now you can launch the app and modify whatever you want in the render() method. simply hit ^= or Product>Inject source and you'll see your changes Live !
Installation
CocoaPods
pod 'SteviaLayout'
use_frameworks!Carthage
github "s4cha/Stevia"-
Create a
Cartfilefile at the root of your project folder -
Add
github "s4cha/Stevia"to your Cartfile -
Run
carthage update -
Drag and drop
Stevia.frameworkfrom/Carthage/Build/iOS/to Linked frameworks and libraries in Xcode (Project>Target>General>Linked frameworks and libraries) -
Add new run script (Project>Target>Build Phases>+> New run script phase)
/usr/local/bin/carthage copy-frameworks -
Add Input files
$(SRCROOT)/Carthage/Build/iOS/Stevia.framework
There you go!
Manual
Copy Stevia source files to your Xcode project
Documentation
View Hierarchy
sv(
subview1,
subview2,
subview3
)sv([]) and sv() are essentially shortcuts that call addSubview() and
view.translatesAutoresizingMaskIntoConstraints = false
It also has the benefit of being very visual so that your can actually see what the view hierarchy is. This is especially true for nested hierarchies :
sv(
subview1,
subview2.sv(
nestedView1,
nestedView2Μ¨
),
subview3
)Horizontal layout
This is intended to look like Apple's visual format, so you should be very familiar with the syntax.
Stevia only removes the [] and the String.
Stick a label to the left of the screen
|labelWith the default margin (8)
|-labelWith a custom margin
|-42-labelJust to be very clear we want to emphasize that this is pure syntactic sugar.
This equivalent of the following using the chainable api :
label.left(42)Which in turn will create Native Autolayout constraints :
label.superview?.addConstraint(
NSLayoutConstraint(
item: label,
attribute:.Left,
relatedBy: .Equal,
toItem: label.superview!,
attribute:.Left,
multiplier: 1,
constant: 42
)
)Combine all at once.
|-avatar-15-name-20-followButton-|Vertical layout
avatar.top(50)==
layout(
50,
avatar
)While using layout for a single element might seem a bit overkill, it really shines when combined with horizontal layout.
Then we have the full layout in one place (hence the name).
layout(
50,
|-15-avatar.size(60)
)The avatar is 50px from the top with a left margin of 15px and a size of 60px
Another great example is the login view, representable in one single statement !
layout(
100,
|-email-| ~ 80,
8,
|-password-| ~ 80,
"",
|login| ~ 80,
0
)In case you wonder ~ operator == .height(x), it's just more readable in a layout statement that way.
Chainable Api
The avatar example above could've been written that way using the chainable api :
avatar.top(50).left(15).size(50)Using layout is just clearer in most of the cases but it's yours to choose which way you prefer :)
Centering
Horizontally
imageView.centerHorizontally()Vertically
imageView.centerVertically()On both axis
imageView.centerInContainer()Alignment
Horizontally
alignHorizontally(avatar,name,followButton)Vertically
alignVertically(title,subtitle,text)Align the center of one view with another one :
alignCenter(view1, with: view2)In the example above of a follow Cell, here is how the layout code would look like :
|-avatar-15-name-20-followButton-|
alignHorizontally(avatar,name,followButton)But |-avatar-15-name-20-followButton-| actually returns the array of views!!! so we can write it in one single statement :
alignHorizontally(|-avatar-15-name-20-followButton-|)Button taps
button.addTarget(self, action: "follow", forControlEvents: .TouchUpInside)Becomes:
button.tap(follow)This is shorter and less error-prone since follow is is not referenced by a string value anymore \o/
Styles
Well, just call style on a UIView subclass :
In-line for small or unique styles
detail.style { l in
l.numberOfLines = 0
l.textAlignment = .Center
l.textColor = .blueColor()
l.text = NSLocalizedString("NeedPetMessage", comment: "")
}Or in a separate to make them reusable
// My style method, kinda like CSS
func detailStyle(l:UILabel) {
l.numberOfLines = 0
l.textAlignment = .Center
l.textColor = .blueColor()
l.text = NSLocalizedString("NeedPetMessage", comment: "")
}
// Later
{
// Set my style
detail.style(detailStyle)
}This is the preferred way because the styles become reusable and composable: you can chain them! You can even create a Style File grouping high level functions for common styles. Usage then becomes very similar to CSS!
Content
button.setTitle("Hello", forState: .Normal)Becomes :
button.text("Hello")button.setImage(UIImage(named:"CommentIcon"), forState: .Normal)Becomes :
button.image("CommentIcon")Rationale behind the project
On the Yummypets app, we needed to deal with looooooots of views.
After trying different methods for building views (Xibs, Storyboards, Splitting Storyboards, React Native even(!).
We found that coding views programmatically was the best solution for us.
But coding views programmatically had its issues too: UIKit exposes an imperative, verbose API, and it's really easy to create a mess with it.
That's why we created Stevia.
Contributors
YannickDot, S4cha, Damien, Snowcraft, Mathieu-o
Other repos β€οΈ
Stevia

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.


