An excellent tutorial on Storyboards (from Matthijs')
Storyboards Tutorial in iOS 7: Part 1
Note from Ray: Tutorial Team member Matthijs Hollemans (the iOS Apprentice Series author) has ported this popular tutorial from iOS 5 by Tutorials to iOS 7. This is a sneak peek of the third edition of the book, which will be updated to iOS 7. We hope you enjoy!
Storyboarding is an exciting feature first introduced with iOS 5 that saves you a lot of time building user interfaces for your apps.
To show you what a storyboard is, I’ll let a picture do the talking. This is the storyboard that you will be building in this tutorial:
You may not know exactly yet what the app does but you can clearly see which screens it has and how they are related. That is the power of using storyboards.
If you have an app with many different screens then storyboards can help reduce the amount of glue code you have to write to go from one screen to the next. Instead of using a separate nib file for each view controller, your app uses a single storyboard that contains the designs of all of these view controllers and the relationships between them.
Storyboards have a number of advantages over regular nibs:
- With a storyboard you have a better conceptual overview of all the screens in your app and the connections between them. It’s easier to keep track of everything because the entire design is in a single file, rather than spread out over many separate nibs.
- The storyboard describes the transitions between the various screens. These transitions are called “segues” and you create them by simply ctrl-dragging from one view controller to the next. Thanks to segues you need less code to take care of your UI.
- Storyboards make working with table views a lot easier with the new prototype cells and static cells features. You can design your table views almost completely in the storyboard editor, something else that cuts down on the amount of code you have to write.
If you’re the type who hates Interface Builder and who really wants to create his entire UI programmatically, then storyboards are probably not for you. Personally, I prefer to write as little code as possible — especially UI code! — so this tool is a welcome addition to my arsenal.
And if you want to keep using nibs then go right ahead, but know that you can combine storyboards with nibs. It’s not an either-or situation.
In this tutorial you’ll take a look at what you can do with storyboards. You’re going to build a simple app that lets you create a list of players and games, and rate their skill levels. In the process, you’ll learn the most common tasks that you’ll be using storyboards for.
Storyboards tutorial: iOS 7 style
Fire up Xcode and create a new project. You’ll use the Single View Application template as the starting point and then build up the app from there.Fill in the template options as follows:
- Product Name: Ratings
- Organization Name: fill this in however you like
- Company Identifier: the identifier that you use for your apps, in reverse domain notation
- Class Prefix: leave this empty
- Devices: iPhone
After Xcode has created the project, the main Xcode window looks like this:
The new project consists of two classes, AppDelegate and ViewController, and the star of this tutorial: the Main.storyboard file. Notice that there are no .xib files in the project.
This is a portrait-only app, so before you continue, uncheck the Landscape Left and Landscape Right options under Deployment Info, Device Orientation.
Let’s take a look at that storyboard. Click Main.storyboard in the list of files to open it in Interface Builder:
Editing storyboards in Interface Builder works pretty much the same way as editing nibs. You can drag new controls from the Object Library (see bottom-right corner) into your view controller to design its layout. The difference is that the storyboard doesn’t contain just one view controller from your app, but all of them.
The official storyboard terminology for a view controller is “scene”, but you can use the terms interchangeably. The scene is what represents the view controller in the storyboard. Previously you would use a separate nib for each scene / view controller, but now they are all combined into a single storyboard.
On the iPhone only one of these scenes is visible at a time, but on the iPad you can show several at once, for example the master and detail panes in a split-view, or the content of a popover.
Note: Xcode 5 enables Auto Layout by default for storyboard
and nib files. Auto Layout is a cool new technology for making flexible
user interfaces that can easily resize, which is useful on the iPad and
for supporting the larger iPhone 5, but it only works on iOS 6 and up.
It also has a bit of a learning curve, which is why you’re not using it
in this tutorial. To learn more about Auto Layout, see our books iOS 6 by Tutorials and iOS 7 by Tutorials.
Disable Auto Layout from the File inspector for the storyboard:To get some feel for how the storyboard editor works, drag some controls into the blank view controller:
Find this button at the bottom of the storyboard canvas:
Click it to open the Document Outline in the sidebar on the left:
When editing a nib this area lists just the components from that one nib, but for a storyboard it shows the contents of all your view controllers. Currently there is only one view controller (or scene) in your storyboard but in the course of this tutorial you’ll be adding several others.
There is a miniature version of this Document Outline below the scene, named the Dock:
The Dock shows the top-level objects in the scene. Each scene has at least a View Controller object, a First Responder object, and an Exit item, but it can potentially have other top-level objects as well. The Dock is convenient for making connections to outlets and actions. If you need to connect something to the view controller, you can simply drag to its icon in the Dock.
Note: You probably won’t be using the First Responder very
much. This is a proxy object that refers to whatever object has first
responder status at any given time. It was also present in your nibs and
you probably never had a need to use it then either. As an example, you
can hook up the Touch Up Inside event from a button to First
Responder’s cut: selector. If at some point a text field has input focus
then you can press that button to make the text field, which is now the
first responder, cut its text to the pasteboard.
Run the app and it should look exactly like what you designed in the
editor (shown here on the 4-inch Retina iPhone simulator running iOS
7.0):If you’ve ever made a nib-based app before then you always had a MainWindow.xib file. This nib contained the top-level UIWindow object, a reference to the App Delegate, and one or more view controllers. When you put your app’s UI in a storyboard, however, MainWindow.xib is no longer used. So how does the storyboard get loaded by the app?
Let’s take a peek at the application delegate. Open up AppDelegate.h and you’ll see it looks like this:
#import |
UIResponder
and that it has a UIWindow
property. If you look into AppDelegate.m, you’ll see that it does absolutely nothing. All the methods are practically empty. Even application:didFinishLaunchingWithOptions:
simply returns YES. The secret is in the Ratings-Info.plist file. Click on Ratings-Info.plist (you can find it in the Supporting Files group) and you’ll see this:
Storyboard apps use the key
UIMainStoryboardFile
, or
“Main storyboard file base name”, to specify the name of the storyboard
that must be loaded when the app starts. When this setting is present, UIApplication
will load the named storyboard file and automatically instantiates the
first view controller from that storyboard, then puts its view into a
new UIWindow
object. No programming necessary.You can also see this in the Project Settings screen in the Deployment Info section:
For the sake of completeness, also open main.m to see what’s in there:
#import |
UIApplicationMain()
, otherwise it won’t be able to find it.Just Add It To My Tab
The Ratings app has a tabbed interface with two screens. With a storyboard it is really easy to create tabs.Switch back to Main.storyboard, and drag a Tab Bar Controller from the Object Library into the canvas. You may want to maximize your Xcode window first, because the Tab Bar Controller comes with two view controllers attached and you’ll need some room to maneuver. You can zoom out using the little floating panel in the bottom-right corner of the canvas.
The new Tab Bar Controller comes pre-configured with two other view controllers, one for each tab. UITabBarController is a so-called container view controller because it contains one or more other view controllers. Two other common containers are the Navigation Controller and the Split View Controller (you’ll see both of them later).
The container relationship is represented by the arrows between the Tab Bar Controller and the view controllers that it contains.
Note: If you want to move the Tab Bar Controller and its attached view controllers as a group, you can ⌘-click to select multiple scenes and then move them around together. (Selected scenes have a thick blue outline.)
Drag a label into the first view controller and give it the text “First Tab”. Also drag a label into the second view controller and name it “Second Tab”. This allows you to actually see something happen when you switch between the tabs.
Note: You can’t drag stuff into the scenes when the editor is zoomed out. You’ll need to return to the normal zoom level first. You can do that quickly by double-clicking in the canvas.
Select the Tab Bar Controller and go to the Attributes inspector. Check the box that says Is Initial View Controller.
In the canvas the arrow that at first pointed to the regular view controller now points at the Tab Bar Controller:
This means that when you run the app,
UIApplication
will make the Tab Bar Controller the main screen. The storyboard always has a single view controller that is designated the initial view controller, that serves as the entry point into the storyboard.Tip: To change the initial view controller, you can also drag the arrow between view controllers.
Run the app and try it out. The app now has a tab bar and you can switch between the two view controllers with the tabs:
Xcode actually comes with a template for building a tabbed app (unsurprisingly called the Tabbed Application template) that you could have used, but it’s good to know how this works so you can also create a Tab Bar Controller by hand if you have to.
Note: If you connect more than five scenes to the Tab Bar
Controller, it automatically gets a More… tab when you run the app.
Pretty neat!
Remove the view controller that was originally added by the template,
as you’ll no longer be using it. The storyboard now contains just the
tab bar and the two scenes for its tabs.Adding a Table View Controller
The two scenes that are currently attached to the Tab Bar Controller are both regularUIViewController
s. You are going to replace the scene from the first tab with a UITableViewController
instead.Click on that first view controller to select it, and then delete it. From the Object Library drag a new Table View Controller into the canvas in the place where that previous scene used to be:
With the Table View Controller selected, choose Editor\Embed In\Navigation Controller from Xcode’s menubar. This adds yet another view controller to the canvas:
You could also have dragged in a Navigation Controller from the Object Library, but this Embed In command is just as easy.
Because the Navigation Controller is also a container view controller (just like the Tab Bar Controller), it has a relationship arrow pointing at the Table View Controller. You can also see these relationships in the Document Outline:
Notice that embedding the Table View Controller gave it a navigation bar. Interface Builder automatically put it there because this scene will now be displayed inside the Navigation Controller’s frame. It’s not a real
UINavigationBar
object but a simulated one.If you look at the Attributes inspector for the Table View Controller, you’ll see the Simulated Metrics section at the top:
“Inferred” is the default setting for storyboards and it means the scene will show a navigation bar when it’s inside of a Navigation Controller, a tab bar when it’s inside of a Tab Bar Controller, and so on. You could override these settings if you wanted to, but keep in mind they are here only to help you design your screens. The Simulated Metrics aren’t used during runtime; they’re just a visual design aid that shows what your screen will end up looking like.
Let’s connect these two new scenes to the Tab Bar Controller. Ctrl-drag from the Tab Bar Controller to the Navigation Controller:
When you let go, a small popup menu appears:
Choose the Relationship Segue – view controllers option. This creates a new relationship arrow between the two scenes:
The Tab Bar Controller has two such relationships, one for each tab. The Navigation Controller itself has a relationship connection to the Table View Controller. There is also another type of arrow, the segue, that we’ll talk about later.
When you made this new connection, a new tab was added to the Tab Bar Controller, simply named “Item”. For this app, you want this new scene to be the first tab, so drag the tabs around to change their order:
Run the app and try it out. The first tab now contains a table view inside a navigation controller.
Before you put some actual functionality into this app, let’s clean up the storyboard a little. You will name the first tab “Players” and the second “Gestures”. Unlike what you may expect, you do not change this on the Tab Bar Controller itself, but in the view controllers that are connected to these tabs.
As soon as you connect a view controller to the Tab Bar Controller, it is given a Tab Bar Item object. You use the Tab Bar Item to configure the tab’s title and image.
Select the Tab Bar Item inside the Navigation Controller, and in the Attributes inspector set its Title to Players:
Rename the Tab Bar Item for the view controller from the second tab to Gestures.
A well-designed app should also put some pictures on these tabs. The resources for this tutorial contains a subfolder named Images. Add that folder to the project. In the Attributes inspector for the Players Tab Bar Item, choose the Players.png image. You probably guessed it, but give the Gestures item the image Gestures.png.
A view controller that is embedded inside a Navigation Controller has a Navigation Item that is used to configure the navigation bar. Select the Navigation Item for the Table View Controller (you can find it in the Document Outline) and change its title in the Attributes inspector to Players.
Alternatively, you can double-click the navigation bar and change the title there. (Note: you should double-click the simulated navigation bar in the Table View Controller, not the actual Navigation Bar object in the Navigation Controller.)
Run the app and marvel at your pretty tab bar, all without writing a single line of code!
Prototype cells
Prototype cells allow you to easily design a custom layout for your table view cells directly from within the storyboard editor.The Table View Controller comes with a blank prototype cell. Click on that cell to select it and in the Attributes inspector set the Style option to Subtitle. This immediately changes the appearance of the cell to include two labels.
If you’ve used table views before and created your own cells by hand, you may recognize this as the
UITableViewCellStyleSubtitle
style. With prototype cells you can either pick one of the built-in
cell styles as you just did, or create your own custom design (which
you’ll do shortly).Set the Accessory attribute to Disclosure Indicator and in the Identifier field type PlayerCell. All prototype cells are still regular
UITableViewCell
objects and therefore should have a reuse identifier.Run the app, and… nothing has changed. That’s not so strange: you still have to make a data source for the table so it will know what rows to display.
Add a new file to the project. Choose the Objective-C class template. Name the class PlayersViewController and make it a subclass of UITableViewController. The With XIB for user interface option should be unchecked because you already have the design of this view controller in the storyboard. No nibs today!
Go back to the storyboard and select the Table View Controller (make sure you select the actual view controller and not one of the views inside it). In the Identity inspector, set its Class to PlayersViewController. That is the essential step for hooking up a scene from the storyboard with your own view controller subclass. Don’t forget this or your class won’t be used!
From now on when you run the app that table view controller from the storyboard is an instance of the
PlayersViewController
class.Add a mutable array property to PlayersViewController.h:
#import |
Player
objects. Add a new file to the project using the Objective-C class template. Name it Player, subclass of NSObject.Change Player.h to the following:
@interface Player : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *game; @property (nonatomic, assign) int rating; @end |
Player
is simply a
container object for these three properties: the name of the player,
the game he’s playing, and a rating of 1 to 5 stars.You’ll make the array and some test
Player
objects in the App Delegate and then assign it to the PlayersViewController
’s players
property.In AppDelegate.m, add an #import for the
Player
and PlayersViewController
classes at the top of the file, and add a new instance variable named _players
:#import "AppDelegate.h" #import "Player.h" #import "PlayersViewController.h" @implementation AppDelegate { NSMutableArray *_players; } // Rest of file... |
application:didFinishLaunchingWithOptions:
method to:- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _players = [NSMutableArray arrayWithCapacity:20]; Player *player = [[Player alloc] init]; player.name = @"Bill Evans"; player.game = @"Tic-Tac-Toe"; player.rating = 4; [_players addObject:player]; player = [[Player alloc] init]; player.name = @"Oscar Peterson"; player.game = @"Spin the Bottle"; player.rating = 5; [_players addObject:player]; player = [[Player alloc] init]; player.name = @"Dave Brubeck"; player.game = @"Texas Hold’em Poker"; player.rating = 2; [_players addObject:player]; UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController; UINavigationController *navigationController = [tabBarController viewControllers][0]; PlayersViewController *playersViewController = [navigationController viewControllers][0]; playersViewController.players = _players; return YES; } |
Player
objects and adds them to the _players
array. But then it does the following:UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController; UINavigationController *navigationController = [tabBarController viewControllers][0]; PlayersViewController *playersViewController = [navigationController viewControllers][0]; playersViewController.players = _players; |
_players
array to the players
property of PlayersViewController
so it can use this array for its data source. But the app delegate doesn’t know anything about PlayersViewController
yet, so it will have to dig through the storyboard to find it.
Note: This is one of the limitations of storyboards. With
nibs you always had a reference to the App Delegate in your
MainWindow.xib and you could make connections from your top-level view
controllers to outlets on the App Delegate. That is currently not
possible with storyboards. You cannot make references to the app
delegate from your top-level view controllers. That’s unfortunate, but
you can always get those references programmatically, which is what you
do here.
Let’s take it step-by-step:UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController; |
rootViewController
and cast it to UITabBarController
.The
PlayersViewController
sits inside a navigation controller in the first tab, so you first look up that UINavigationController
object,UINavigationController *navigationController = [tabBarController viewControllers][0]; |
PlayersViewController
that you are looking for:PlayersViewController *playersViewController = [navigationController viewControllers][0]; |
Now that you have an array full of
Player
objects, you can continue building the data source for PlayersViewController
. Open up PlayersViewController.m and add an import at the top:#import "Player.h"
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.players count]; } |
cellForRowAtIndexPath
. Previously, this method typically looked something like this:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... return cell; } |
nil
because there were no free cells to reuse, you would create a new
instance of the cell class. That is no doubt how you’ve been writing
your own table view code all this time. Well, no longer!Replace that method with:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; Player *player = (self.players)[indexPath.row]; cell.textLabel.text = player.name; cell.detailTextLabel.text = player.game; return cell; } |
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; |
Run the app, and lo and behold, the table view has players in it:
It just takes one line of code to use these newfangled prototype cells. I think that’s just great!
Note: In this app you’re using only one prototype cell but if your table needs to display different kinds of cells then you can simply add additional prototype cells to the storyboard. You can either duplicate the existing cell to make a new one, or increment the value of the Table View’s Prototype Cells attribute. Be sure to give each cell its own re-use identifier, though.
Designing Your Own Prototype Cells
Using a standard cell style is OK for many apps, but for this app you want to add an image on the right-hand side of the cell that shows the player’s rating (one to five stars). Having an image view in that spot is not supported by the standard cell styles, so you’ll have to make a custom design.Switch back to Main.storyboard, select the prototype cell in the table view, and set its Style attribute to Custom. The default labels now disappear.
First make the cell a little taller. Either drag its handle at the bottom or change the Row Height value in the Size inspector. Make the cell 55 points high.
Drag two Label objects from the Objects Library into the cell and place them roughly where the standard labels were previously. Just play with the font and colors and pick something you like.
Drag an Image View into the cell and place it on the right, next to the disclosure indicator. Make it 81 points wide; the height isn’t very important. Set its Mode to Center (under View in the Attributes inspector) so that whatever image you put into this view is not stretched.
Make the labels 190 points wide so they don’t overlap with the image view. The final design for the prototype cell looks something like this:
Because this is a custom designed cell, you can no longer use
UITableViewCell
’s textLabel
and detailTextLabel
properties to put text into the labels. These properties refer to
labels that aren’t on this cell anymore; they are only valid for the
standard cell types. Instead, you will use tags to find the labels.Give the Name label tag 100, the Game label tag 101, and the Image View tag 102. You can do this in the Attributes inspector.
Then open PlayersViewController.m and add a new method,
imageForRating:
.- (UIImage *)imageForRating:(int)rating { switch (rating) { case 1: return [UIImage imageNamed:@"1StarSmall"]; case 2: return [UIImage imageNamed:@"2StarsSmall"]; case 3: return [UIImage imageNamed:@"3StarsSmall"]; case 4: return [UIImage imageNamed:@"4StarsSmall"]; case 5: return [UIImage imageNamed:@"5StarsSmall"]; } return nil; } |
tableView:cellForRowAtIndexPath:
to the following:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; Player *player = (self.players)[indexPath.row]; UILabel *nameLabel = (UILabel *)[cell viewWithTag:100]; nameLabel.text = player.name; UILabel *gameLabel = (UILabel *)[cell viewWithTag:101]; gameLabel.text = player.game; UIImageView *ratingImageView = (UIImageView *)[cell viewWithTag:102]; ratingImageView.image = [self imageForRating:player.rating]; return cell; } |
Hmm, that doesn’t look quite right, the cells appear to overlap one another. You did change the height of the prototype cell but the table view doesn’t necessarily take that into consideration. There are two ways to fix it: you can change the table view’s Row Height attribute or implement the
tableView:heightForRowAtIndexPath:
method. The former is much easier, so let’s do that.
Note: You would use
Back in Main.storyboard, in the Size inspector of the Table View, set Row Height to 55:heightForRowAtIndexPath
if you did not know the height of your cells in advance, or if different rows can have different heights.If you run the app now, it looks a lot better!
By the way, if you changed the height of the cell by dragging its handle rather than typing in the value, then the table view’s Row Height property was automatically changed too. So it may have worked correctly for you the first time around.
Using a Subclass for the Cell
The table view already works pretty well but I’m not a big fan of using tags to access the labels and other subviews of the prototype cell. It would be much more handy if you could connect these labels to outlets and then use the corresponding properties. As it turns out, you can.Add a new file to the project, with the Objective-C class template. Name it PlayerCell and make it a subclass of UITableViewCell.
Change PlayerCell.h to:
@interface PlayerCell : UITableViewCell @property (nonatomic, weak) IBOutlet UILabel *nameLabel; @property (nonatomic, weak) IBOutlet UILabel *gameLabel; @property (nonatomic, weak) IBOutlet UIImageView *ratingImageView; @end |
nameLabel
, gameLabel
and ratingImageView
, all of which are IBOutlets
.Back in Main.storyboard, select the prototype cell and change its Class to PlayerCell on the Identity inspector. Now whenever you ask the table view for a new cell with
dequeueReusableCellWithIdentifier:
, it returns a PlayerCell
instance instead of a regular UITableViewCell
.Note that you gave this class the same name as the reuse identifier — they’re both called PlayerCell — but that’s only because I like to keep things consistent. The class name and reuse identifier have nothing to do with each other, so you could name them differently if you wanted to.
Now connect the labels and the image view to these outlets. Select the label and drag from New Referencing Outlet in its Connections inspector to the table view cell and select nameLabel and gameLabel, respectively:
Important: You should hook up the controls to the table view
cell, not to the view controller! You see, whenever your data source
asks the table view for a new cell with
This means there will be more than one instance of
Now that you’ve hooked up the properties, you can simplify the data source code one more time. First import the dequeueReusableCellWithIdentifier
, the table view doesn’t give you the actual prototype cell but a copy (or one of the previous cells is recycled if possible).This means there will be more than one instance of
PlayerCell
at any given time. If you were to connect a label from the cell to an
outlet on the view controller, then several copies of the label will try
to use the same outlet. That’s just asking for trouble. (On the other
hand, connecting the prototype cell to actions on the view controller is
perfectly fine. You would do that if you had custom buttons or other UIControls
on your cell.)PlayerCell
class in PlayersViewController.m:#import "PlayerCell.h"
|
cellForRowAtIndexPath
to:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { PlayerCell *cell = (PlayerCell *)[tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; Player *player = (self.players)[indexPath.row]; cell.nameLabel.text = player.name; cell.gameLabel.text = player.game; cell.ratingImageView.image = [self imageForRating:player.rating]; return cell; } |
dequeueReusableCellWithIdentifier
to a PlayerCell
,
and then you can simply use the properties that are wired up to the
labels and the image view. Isn’t it great how using prototype cells
makes table views a whole lot less messy?Run the app and try it out. It should still look the same as before, but behind the scenes it’s now using your own table view cell subclass!
Where To Go From Here?
Check out part two of this tutorial, where we’ll cover segues, static table view cells, the Add Player screen, a game picker screen, and the downloadable example project for this tutorial!This Beginning Storyboards in iOS 7 series is an update preview of one of the chapters in our iOS 5 By Tutorials book. If you like what you see here, check out the book — there’s an entire second chapter on intermediate storyboarding, above and beyond what we’re posting for free here! We also update our all books to the latest versions of iOS so an update of this book for iOS 7 is coming out soon :]
If you felt lost at any point during this tutorial, you also might want to brush up on the basics with my newly updated iOS Apprentice series. In that series, I cover the foundational knowledge you need as an iOS developer from the ground up — perfect for complete beginners, or those looking to fill in some gaps.
If you have any questions or comments on this tutorial or on storyboarding, please join the forum discussion below!