Yoga is a cross-platform layout engine based on Flexbox that makes working with layouts easy. Instead of using Auto Layout for iOS or using Cascading Style Sheets (CSS) on the web, you can use Yoga as a common layout system.
Initially launched as css-layout, an open source library from Facebook in 2014, it was revamped and rebranded as Yoga in 2016. Yoga supports multiple platforms including Java, C#, C, and Swift.
Library developers can incorporate Yoga into their layout systems, as Facebook has done for two of their open source projects: React Native and Litho. However, Yoga also exposes a framework that iOS developers can directly use for laying out views.
In this tutorial, you’ll work through core Yoga concepts then practice and expand them in building the FlexAndChill app.
Even though you’ll be using the Yoga layout engine, it will be helpful for you to be familiar with Auto Layout before reading this tutorial. You’ll also want to have a working knowledge of CocoaPods to include Yoga in your project.
Unpacking Flexbox
Flexbox, also referred to as CSS Flexible Box, was introduced to handle complex layouts on the web. One key feature is the efficient layout of content in a given direction and the “flexing” of its size to fit a certain space.
Flexbox consists of flex containers, each having one or more flex items:
Flexbox defines how flex items are laid out inside of a flex container. Content outside of the flex container and inside of a flex item are rendered as usual.
Flex items are laid out in a single direction inside of a container (although they can be optionally wrapped). This sets the main axis for the items. The opposite direction is known as the cross axis.
Flexbox allows you to specify how items are positioned and spaced on the main axis and the cross axis. justify-content
specifies the alignment of items along the container’s main axis. The example below shows item placements when the container’s flex direction is row
:
flex-start
: Items are positioned at the beginning of the container.flex-end
: Items are positioned at the end of the container.center
: Items are positioned in the middle of the container.space-between
: Items are evenly spaced with the first item placed at the beginning and the last item placed at the end of the container.space-around
: Items are evenly spaced with the equal spacing around them.
align-items
specifies the alignment of items along the container’s cross axis. The example shows item placements when the container’s flex direction is row
which means the cross axis runs vertically:
The items are vertically aligned at the beginning, center, or end of the container.
These initial Flexbox properties should give you a feel for how Flexbox works. There are many more you can work with. Some control how an item stretches or shrinks relative to the available container space. Others can set the padding, margin, or even size.
Flexbox Examples
A perfect place to try out Flexbox concepts is jsFiddle, an online playground for JavaScript, HTML and CSS.
Go to this starter JSFiddle and take a look. You should see four panes:
The code in the three editors drive the output you see in the lower right pane. The starter example displays a white box.
Note the yoga
class selector defined in the CSS editor. These represent the CSS defaults that Yoga implements. Some of the values differ from the Flexbox w3 specification defaults. For example, Yoga defaults to a column
flex direction and items are positioned at the start of the container. Any HTML elements that you style via class="yoga"
will start off in “Yoga” mode.
Check out the HTML source:
<div class="yoga"
style="width: 400px; height: 100px; background-color: white; flex-direction:row;">
</div>
The div
‘s basic style is yoga
. Additional style
properties set the size, background, and overrides the default flex direction so that items will flow in a row.
In the HTML editor, add the following code just above the closing div
tag:
<div class="yoga" style="background-color: #cc0000; width: 80px;"></div>
This adds a yoga
styled, 80-pixel wide, red box to the div
container.
Tap Run in the top menu. You should see the following output:
Add the following child element to the root div
, right after the red box’s div
:
<div class="yoga" style="background-color: #0000cc; width: 80px;"></div>
This adds an 80-pixel wide blue box.
Tap Run. The updated output shows the blue box stacked to the right of the red box:
Replace the blue box’s div
code with the following:
<div class="yoga" style="background-color: #0000cc; width: 80px; flex-grow: 1;"></div>
The additional flex-grow
property allows the box to expand and fill any available space.
Tap Run to see the updated output with the blue box stretched out:
Replace the entire HTML source with the following:
<div class="yoga"
style="width: 400px; height: 100px; background-color: white; flex-direction:row; padding: 10px;">
<div class="yoga" style="background-color: #cc0000; width: 80px; margin-right: 10px;"></div>
<div class="yoga" style="background-color: #0000cc; width: 80px; flex-grow: 1; height: 25px; align-self: center;"></div>
</div>
This introduces padding to the child items, adds a right margin to the red box, sets the height of the blue box, and aligns the blue box to the center of the container.
Tap Run to view the resulting output:
You can view the final jsFiddle here. Feel free to play around with other layout properties and values.
Yoga vs. Flexbox
Even though Yoga is based on Flexbox, there are some differences.
Yoga doesn’t implement all of CSS Flexbox. It skips non-layout properties such as setting the color. Yoga has modified some Flexbox properties to provide better Right-to-Left support. Lastly, Yoga has added a new AspectRatio
property to handle a common need when laying out certain elements such as images.
Introducing YogaKit
While you may want to stay in wonderful world wide web-land, this is a Swift tutorial. Fear not, the Yoga API will keep you basking in the afterglow of Flexbox familiarity. You’ll be able to apply your Flexbox learnings to your Swift app layout.
Yoga is written in C, primarily to optimize performance and for easy integration with other platforms. To develop iOS apps, you’ll be working with YogaKit, which is a wrapper around the C implementation.
Recall that in the Flexbox web examples, layout was configured via style attributes. With YogaKit, layout configuration is done through a YGLayout
object. YGLayout
includes properties for flex direction, justify content, align items, padding, and margin.
YogaKit exposes YGLayout
as a category on UIView
. The category adds a configureLayout(block:)
method to UIView
. The block
closure takes in a YGLayout
parameter and uses that info to configure the view’s layout properties.
You build up your layout by configuring each participating view with the desired Yoga properties. Once done, you call applyLayout(preservingOrigin:)
on the root view’s YGLayout
. This calculates and applies the layout to the root view and subviews.
Your First Layout
Create a new Swift iPhone project with the Single View Application template named YogaTryout.
You’ll be creating your UI programmatically so you won’t need to use storyboards.
Open Info.plist and delete the Main storyboard file base name
property. Then, set the Launch screen interface file base name
value to an empty string. Finally, delete Main.storyboard and LaunchScreen.storyboard.
Open AppDelegate.swift and add the following to application(_:didFinishLaunchingWithOptions:)
before the return
statement:
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.backgroundColor = .white
window?.makeKeyAndVisible()
Build and run the app. You should see a blank white screen.
Close the Xcode project.
Open Terminal and enter the following command to install CocoaPods if you don’t already have it:
sudo gem install cocoapods
In Terminal, go to the directory where YogaTryout.xcodeproj is located. Create a file named Podfile and set its content to the following:
platform :ios, '10.3'
use_frameworks!
target 'YogaTryout' do
pod 'YogaKit', '~> 1.5'
end
Run the following command in Terminal to install the YogaKit
dependency:
pod install
You should see output similar to the following:
Analyzing dependencies
Downloading dependencies
Installing Yoga (1.5.0)
Installing YogaKit (1.5.0)
Generating Pods project
Integrating client project
[!] Please close any current Xcode sessions and use `YogaTryout.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed.
From this point onwards, you’ll be working with YogaTryout.xcworkspace.
Open YogaTryout.xcworkspace then build and run. You should still see a blank white screen.
Open ViewController.swift and add the following import:
import YogaKit
This imports the YogaKit
framework.
Add the following to the end of viewDidLoad()
:
// 1
let contentView = UIView()
contentView.backgroundColor = .lightGray
// 2
contentView.configureLayout { (layout) in
// 3
layout.isEnabled = true
// 4
layout.flexDirection = .row
layout.width = 320
layout.height = 80
layout.marginTop = 40
layout.marginLeft = 10
}
view.addSubview(contentView)
// 5
contentView.yoga.applyLayout(preservingOrigin: true)
This code does the following:
- Creates a view and sets the background color.
- Sets up the layout configuration closure.
- Enables Yoga styling during this view’s layout.
- Sets various layout properties including the flex direction, frame size, and margin offsets.
- Calculates and applies the layout to
contentView.
Build and run the app on an iPhone 7 Plus simulator. You should see a gray box:
You may be scratching your head, wondering why you couldn’t have simply instantiated a UIView
with the desired frame size and set its background color. Patience my child. The magic starts when you add child items to this initial container.
Add the following to viewDidLoad()
just before the line that applies the layout to contentView
:
let child1 = UIView()
child1.backgroundColor = .red
child1.configureLayout{ (layout) in
layout.isEnabled = true
layout.width = 80
}
contentView.addSubview(child1)
This code adds an 80-pixel wide red box to contentView
.
Now, add the following just after the previous code:
let child2 = UIView()
child2.backgroundColor = .blue
child2.configureLayout{ (layout) in
layout.isEnabled = true
layout.width = 80
layout.flexGrow = 1
}
contentView.addSubview(child2)
This adds a blue box to the container that’s 80 pixels wide but that’s allowed to grow to fill out any available space in the container. If this is starting to look familiar, it’s because you did something similar in jsFiddle.
Build and run. You should see the following:
Now, add the following statement to the layout configuration block for contentView
:
layout.padding = 10
This sets a padding for all the child items.
Add the following to child1
‘s layout configuration block:
layout.marginRight = 10
This sets a right margin offset for the red box.
Finally, add the following to child2
‘s layout configuration block:
layout.height = 20
layout.alignSelf = .center
This sets the height of the blue box and aligns it to the center of its parent container.
Build and run. You should see the following:
What if you want to center the entire gray box horizontally? Well, you can enable Yoga on contentView
‘s parent view which is self.view
.
Add the following to viewDidLoad()
, right after the call to super
:
view.configureLayout { (layout) in
layout.isEnabled = true
layout.width = YGValue(self.view.bounds.size.width)
layout.height = YGValue(self.view.bounds.size.height)
layout.alignItems = .center
}
This enables Yoga for the root view and configures the layout width and height based on the view bounds. alignItems
configures the child items to be center-aligned horizontally. Remember that alignItems
specifies how a container’s child items are aligned in the cross axis. This container has the default column
flex direction. So the cross axis is in the horizontal direction.
Remove the layout.marginLeft
assignment in contentView
‘s layout configuration. It’s no longer needed as you’ll be centering this item through its parent container.
Finally, replace:
contentView.yoga.applyLayout(preservingOrigin: true)
With the following:
view.yoga.applyLayout(preservingOrigin: true)
This will calculate and apply the layout to self.view
and its subviews which includes contentView
.
Build and run. Note that the gray box is now centered horizontally:
Centering the gray box vertically on the screen is just as simple. Add the following to the layout configuration block for self.view
:
layout.justifyContent = .center
Remove the layout.marginTop
assignment in contentView
‘s layout configuration. It won’t be needed since the parent is controlling the vertical alignment.
Build and run. You should now see the gray box center-aligned both horizontally and vertically:
Rotate the device to landscape mode. Uh-oh, you’ve lost your center:
Fortunately, there’s a way to get notified about device orientation changes to help resolve this.
Add the following method to the end of the class:
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// 1
view.configureLayout{ (layout) in
layout.width = YGValue(size.width)
layout.height = YGValue(size.height)
}
// 2
view.yoga.applyLayout(preservingOrigin: true)
}
The code does the following:
- Updates the layout configuration with the size of the new orientation. Note that only the affected properties are updated.
- Re-calculates and applies the layout.
Rotate the device back to portrait mode. Build and run the app. Rotate the device to landscape mode. The gray box should now be properly centered:
You can download the final tryout project here if you wish to compare with your code.
Granted, you’re probably mumbling under your breath about how you could have built this layout in less than three minutes with Interface Builder, including properly handling rotations:
You’ll want to give Yoga a fresh look when your layout starts to become more complicated than you’d like and things like embedded stack views are giving you fits.
On the other hand, you may have long abandoned Interface Builder for programmatic layout approaches like layout anchors or the Visual Format Language. If those are working for you, no need to change. Keep in mind that the Visual Format Language doesn’t support aspect ratios whereas Yoga does.
Yoga is also easier to grasp once you understand Flexbox. There are many resources where you can quickly try out Flexbox layouts before building them out on iOS with Yoga.
Advanced Layout
Your joy of building white, red, and blue boxes has probably worn thin. Time to shake it up a bit. In the following section, you’ll take your newly minted Yoga skills to create a view similar to the following:
Download and explore the starter project. It already includes the YogaKit dependency. The other main classes are:
- ViewController: Displays the main view. You’ll primarily be working in this class.
- ShowTableViewCell: Used to display an episode in the table view.
- Show: Model object for a show.
Build and run the app. You should see a black screen.
Here’s a wireframe breakdown of the desired layout to help plan things out:
Let’s quickly dissect the layout for each box in the diagram:
- Displays the show’s image.
- Displays summary information for the series with the items laid out in a row.
- Displays title information for the show with the items laid out in a row.
- Displays the show’s description with the items laid out in a column.
- Displays actions that can be taken. The main container is laid out in a row. Each child item is a container with items laid out in a column.
- Displays tabs with items laid out in a row.
- Displays a table view that fills out the remaining space.
As you build each piece of the layout you’ll get a better feel for additional Yoga properties and how to fine tune a layout.
Open ViewController.swift and add the following to viewDidLoad()
, just after the shows are loaded from the plist:
let show = shows[showSelectedIndex]
This sets the show
to be displayed.
Aspect Ratio
Yoga introduces an aspectRatio
property to help lay out a view if an item’s aspect ratio is known. AspectRatio
represents the width-to-height ratio.
Add the following code right after contentView
is added to its parent:
// 1
let episodeImageView = UIImageView(frame: .zero)
episodeImageView.backgroundColor = .gray
// 2
let image = UIImage(named: show.image)
episodeImageView.image = image
// 3
let imageWidth = image?.size.width ?? 1.0
let imageHeight = image?.size.height ?? 1.0
// 4
episodeImageView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexGrow = 1.0
layout.aspectRatio = imageWidth / imageHeight
}
contentView.addSubview(episodeImageView)
Let’s go through the code step-by-step:
- Creates a
UIImageView
- Sets the image based on the selected show
- Teases out the image’s size
- Configures the layout and sets the
aspectRatio
based on the image size
Build and run the app. You should see the image stretch vertically yet respect the image’s aspect ratio:
FlexGrow
Thus far you’ve seen flexGrow
applied to one item in a container. You stretched the blue box in a previous example by setting its flexGrow
property to 1.
If more than one child sets a flexGrow
property, then the child items are first laid out based on the space they need. Each child’s flexGrow
is then used to distribute the remaining space.
In the series summary view, you’ll lay out the child items so that the middle section takes up twice as much left over space as the other two sections.
Add the following after episodeImageView
is added to its parent:
let summaryView = UIView(frame: .zero)
summaryView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexDirection = .row
layout.padding = self.padding
}
This code specifies that the child items will be laid out in a row and include padding.
Add the following just after the previous code:
let summaryPopularityLabel = UILabel(frame: .zero)
summaryPopularityLabel.text = String(repeating: "★", count: showPopularity)
summaryPopularityLabel.textColor = .red
summaryPopularityLabel.configureLayout { (layout) in
layout.isEnabled = true
layout.flexGrow = 1.0
}
summaryView.addSubview(summaryPopularityLabel)
contentView.addSubview(summaryView)
This adds a popularity label and sets its flexGrow
property to 1.
Build and run the app to view the popularity info:
Add the following code just above the line that adds summaryView
to its parent:
let summaryInfoView = UIView(frame: .zero)
summaryInfoView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexGrow = 2.0
layout.flexDirection = .row
layout.justifyContent = .spaceBetween
}
This sets up a new container view for the summary label child items. Note that the flexGrow
property is set to 2. Therefore, summaryInfoView
will take up twice as much remaining space as summaryPopularityLabel
.
Now add the following code right after the previous block:
for text in [showYear, showRating, showLength] {
let summaryInfoLabel = UILabel(frame: .zero)
summaryInfoLabel.text = text
summaryInfoLabel.font = UIFont.systemFont(ofSize: 14.0)
summaryInfoLabel.textColor = .lightGray
summaryInfoLabel.configureLayout { (layout) in
layout.isEnabled = true
}
summaryInfoView.addSubview(summaryInfoLabel)
}
summaryView.addSubview(summaryInfoView)
This loops through the summary labels to display for a show. Each label is a child item to the summaryInfoView
container. That container’s layout specifies that the labels be placed at the beginning, middle, and end.
Build and run the app to see the show’s labels:
To tweak the layout to get the spacing just right, you’ll add one more item to summaryView
. Add the following code next:
let summaryInfoSpacerView =
UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 1))
summaryInfoSpacerView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexGrow = 1.0
}
summaryView.addSubview(summaryInfoSpacerView)
This serves as a spacer with flexGrow
set to 1. summaryView
has 3 child items. The first and third child items will take 25% of any remaining container space while the second item will take 50% of the available space.
Build and run the app to see the properly tweaked layout:
More Examples
Continue building the layout to see more spacing and positioning examples.
Add the following just after the summaryView
code:
let titleView = UIView(frame: .zero)
titleView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexDirection = .row
layout.padding = self.padding
}
let titleEpisodeLabel =
showLabelFor(text: selectedShowSeriesLabel,
font: UIFont.boldSystemFont(ofSize: 16.0))
titleView.addSubview(titleEpisodeLabel)
let titleFullLabel = UILabel(frame: .zero)
titleFullLabel.text = show.title
titleFullLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
titleFullLabel.textColor = .lightGray
titleFullLabel.configureLayout { (layout) in
layout.isEnabled = true
layout.marginLeft = 20.0
layout.marginBottom = 5.0
}
titleView.addSubview(titleFullLabel)
contentView.addSubview(titleView)
The code sets up titleView
as a container with two items for the show’s title.
Build and run the app to see the title:
Add the following code next:
let descriptionView = UIView(frame: .zero)
descriptionView.configureLayout { (layout) in
layout.isEnabled = true
layout.paddingHorizontal = self.paddingHorizontal
}
let descriptionLabel = UILabel(frame: .zero)
descriptionLabel.font = UIFont.systemFont(ofSize: 14.0)
descriptionLabel.numberOfLines = 3
descriptionLabel.textColor = .lightGray
descriptionLabel.text = show.detail
descriptionLabel.configureLayout { (layout) in
layout.isEnabled = true
layout.marginBottom = 5.0
}
descriptionView.addSubview(descriptionLabel)
This creates a container view with horizontal padding and adds a child item for the show’s detail.
Now, add the following code:
let castText = "Cast: \(showCast)";
let castLabel = showLabelFor(text: castText,
font: UIFont.boldSystemFont(ofSize: 14.0))
descriptionView.addSubview(castLabel)
let creatorText = "Creators: \(showCreators)"
let creatorLabel = showLabelFor(text: creatorText,
font: UIFont.boldSystemFont(ofSize: 14.0))
descriptionView.addSubview(creatorLabel)
contentView.addSubview(descriptionView)
This adds two items to descriptionView
for more show details.
Build and run the app to see the complete description:
Next, you’ll add the show’s action views.
Add a private helper method to the ViewController
extension:
func showActionViewFor(imageName: String, text: String) -> UIView {
let actionView = UIView(frame: .zero)
actionView.configureLayout { (layout) in
layout.isEnabled = true
layout.alignItems = .center
layout.marginRight = 20.0
}
let actionButton = UIButton(type: .custom)
actionButton.setImage(UIImage(named: imageName), for: .normal)
actionButton.configureLayout{ (layout) in
layout.isEnabled = true
layout.padding = 10.0
}
actionView.addSubview(actionButton)
let actionLabel = showLabelFor(text: text)
actionView.addSubview(actionLabel)
return actionView
}
This sets up a container view with an image and label that are center-aligned horizontally.
Now, add the following after the descriptionView
code in viewDidLoad()
:
let actionsView = UIView(frame: .zero)
actionsView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexDirection = .row
layout.padding = self.padding
}
let addActionView =
showActionViewFor(imageName: "add", text: "My List")
actionsView.addSubview(addActionView)
let shareActionView =
showActionViewFor(imageName: "share", text: "Share")
actionsView.addSubview(shareActionView)
contentView.addSubview(actionsView)
This creates a container view with two items created using showActionViewFor(imageName:text)
.
Build and run the app to view the actions.
Time to lay out some tabs.
Add a new method to the ViewController
extension:
func showTabBarFor(text: String, selected: Bool) -> UIView {
// 1
let tabView = UIView(frame: .zero)
tabView.configureLayout { (layout) in
layout.isEnabled = true
layout.alignItems = .center
layout.marginRight = 20.0
}
// 2
let tabLabelFont = selected ?
UIFont.boldSystemFont(ofSize: 14.0) :
UIFont.systemFont(ofSize: 14.0)
let fontSize: CGSize = text.size(attributes: [NSFontAttributeName: tabLabelFont])
// 3
let tabSelectionView =
UIView(frame: CGRect(x: 0, y: 0, width: fontSize.width, height: 3))
if selected {
tabSelectionView.backgroundColor = .red
}
tabSelectionView.configureLayout { (layout) in
layout.isEnabled = true
layout.marginBottom = 5.0
}
tabView.addSubview(tabSelectionView)
// 4
let tabLabel = showLabelFor(text: text, font: tabLabelFont)
tabView.addSubview(tabLabel)
return tabView
}
Going through the code step-by-step:
- Creates a container with center-aligned horizontal items.
- Calculates the desired font info based on if the tab is selected or not.
- Creates a view to indicate that a tab is selected<./li>
- Creates a label representing the tab title.
Add the following code after actionsView
has been added to contentView
(in viewDidLoad
_:
let tabsView = UIView(frame: .zero)
tabsView.configureLayout { (layout) in
layout.isEnabled = true
layout.flexDirection = .row
layout.padding = self.padding
}
let episodesTabView = showTabBarFor(text: "EPISODES", selected: true)
tabsView.addSubview(episodesTabView)
let moreTabView = showTabBarFor(text: "MORE LIKE THIS", selected: false)
tabsView.addSubview(moreTabView)
contentView.addSubview(tabsView)
This sets up the tab container view and adds the tab items to the container.
Build and run the app to see your new tabs:
The tab selection is non-functional in this sample app. Most of the hooks are in place if you’re interested in adding it later.
You’re almost done. You just have to add the table view to the end.
Add following code after tabView
has been added to contentView
:
let showsTableView = UITableView()
showsTableView.delegate = self
showsTableView.dataSource = self
showsTableView.backgroundColor = backgroundColor
showsTableView.register(ShowTableViewCell.self,
forCellReuseIdentifier: showCellIdentifier)
showsTableView.configureLayout{ (layout) in
layout.isEnabled = true
layout.flexGrow = 1.0
}
contentView.addSubview(showsTableView)
This code creates and configures a table view. The layout configuration sets the flexGrow
property to 1, allowing the table view to expand to fill out any remaining space.
Build and run the app. You should see a list of episodes included in the view:
Where To Go From Here?
Congratulations! If you’ve made it this far you’re practically a Yoga expert. Roll out your mat, grab the extra special stretch pants, and just breathe. You can download the final tutorial project here.
Check out the Yoga documentation to get more details on additional properties not covered such as Right-to-Left support.
Flexbox specification is a good resource for more background on Flexbox. Flexbox learning resources is a really handy guide for exploring the different Flexbox properties.
I do hope you enjoyed reading this Yoga tutorial. If you have any comments or questions about this tutorial, please join the forum discussion below!
The post Yoga Tutorial: Using a Cross-Platform Layout Engine appeared first on Ray Wenderlich.