Reel Good took our latest prototype and presented it to their board of investors as the cornerstone of their mobile initiative. The extra work we put behind the design of the app must have really sold it, because Reel Good now needs one more set of changes before they take the app into beta.
For the next MVP, Reel Good wants to be able to tap on the movie cell to display full details of the movie on a different screen.
After the meeting with Reel Good, we sat down with our engineering team and sketched out the necessary requirements of this next task and broke them down:
- We have to create a new view controller to present a full screen version of the movie data
- We need to make sure that navigation works to go to and from one of these view controllers.
- The proper movie data has to be transferred from one view controller to the next, so we need to do some data handling
A large part of development is being able to translate feature requests into actual programming work. Taking some time to plan out a course of action before starting to code will likely save you some time in the end. Even then, there absolutely will be unforseen problems that you will encounter. But that's just part of the fun of programming.
- Create a new custom
UIViewController
to display a singleMovie
object's data - Understand segues in storyboard to transition between view controllers
- Better understanding of the delegate pattern in programming.
- General Reference on Xcode (very useful)
- Configuring a Segue in Storyboard - Apple
- Using Segues (lots of great info here)
- Navigation Controller Implementation - tuts+ (helpful reference and example)
- Drag in a
UIViewController
intoMain.storyboard
from the Objects Library in the Utilities Pane and place it next to theMovieTableViewController
- Select the prototype cell (make sure you have the actual
MovieTableViewCell
selected) and Control-Drag to the newUIViewController
- On the outlet menu that pops up, select "Selection Segue > Show"
- Note: "Accessory Selection" is the action to perform when adding an "accessory view." An example of an accessory view is the
>
(called a "chevron") you see all the way to the right on a cell in the Mail or Messages app on an iPhone. - The "Selection" type of segue refers to the action to take when the cell itself is tapped/selected.
- Select the segue object in storyboard (the -> arrow), and in the Attribute Inspector, set it's identifier to
MovieDetailViewSegue
- Drag in a
UIImageView
into the view controller, giving it the following attributes:
8pt
margins at the top, left, and right- A height of
240pt
- Before adding these constraints, make sure the checkmark box for "Constrain to margins" is selected
- Switch the image view's
Content Mode
toAspect Fit
- Drag in 4
UILabel
s below theUIImageView
in a vertical row
- Label them (in order):
Genre
,Location
,Summary
andSummary Text
- Set their fonts to
Roboto Regular - 17pt
, except forSummary Text
which will beRoboto-Light - 14pt
- Set the number of lines for
Summary Text
to 0
- Select all of the
UILabel
s at once, by holding down the Command (โ) Key while clicking on them
- Select Pin and set it to 8pt margins, also making sure that the checkmark for "Constrain to margins" is selected
- Set the Vertical Content Hugging Priority to 1000 for all labels except
Summary Text
. Instead, set the Vertical Compression Resistance ofSummary Text
to 1000
- Your view controller should resemble:
Being able to set constraints in this batching form is one of the nice advantages of using storyboards.
- Add a new file named
MovieDetailViewController
that subclassesUIViewController
and place it in the correct folder and Xcode group - In
Main.storyboard
change the custom class of the view controller we just added to beMovieDetailViewController
- Create outlets for each of the labels and the imageView. Name them:
moviePosterImageView
,genreLabel
,locationLabel
,summaryLabel
, andsummaryFullTextLabel
- Additionally, give
MovieDetailViewController
an instance variable of typeMovie
internal var selectedMovie: Movie!
- This property will hold a reference to the
Movie
from the cell that was tapped.
A UINavigationController
is unique in that it manages a navigation stack, which is a hierarchy of view controllers.
You can think of each view controller as being a card in a stack of cards. When you push a view controller onto the stack, you're putting a new card on top of the stack of cards. That top-most card is what is currently visible on screen. When you pop a view controller, you're taking a card off the top of the stack, and revealing the card/view controller just below it.
It's important to know that all view controllers currently on the stack can be accessed through the navigation controller
The prepare(for:sender:)
method is where we get things ready for displaying a new view controller that has been set up in storyboard.
As mentioned in the documentation for prepare(for:sender:)
, the sender
parameter refers to the object that has requested the segue. In our case, the sender
is expected to be a MovieTableviewCell
. But because the sender
is defined as being of type Any?
, we should do a check to confirm our assumptions. Moreover, we'll need the cell to determine which movie cell was tapped.
// 1. check sender for the cell that was tapped
if let tappedMovieCell: MovieTableViewCell = sender as? MovieTableViewCell {
}
The segue
object is an instance of UIStoryboardSegue
, which has an instance property of indentifier
that refers to the identifier string we gave to the segue earlier in Main.storyboard
(we used MovieDetailViewSegue
). In order to make sure that we have the correct segue (a storyboard can have many segues), we need to check that the identifier matches one that we expect:
// 1. check sender for the cell that was tapped
if let tappedMovieCell: MovieTableViewCell = sender as? MovieTableViewCell {
// 2. check for the right storyboard segue
if segue.identifier == "MovieDetailViewSegue" {
}
}
While generally you should avoid force unwrapping, because we're certain of both the sender
and the storyboard segue
we can say with some certainty that the segue.destination
is going to be a MovieDetailViewController
:
let movieDetailViewController: MovieDetailViewController = segue.destination as! MovieDetailViewController
We now have the tapped cell (sender
) and the instance of MovieDetailViewController
(segue.destination
), but how do we get the Movie
object that corresponds to the cell we selected?
We already wrote a piece of code in cellForRow
that arranged our movieData
by genre, but that required having the current indexPath
. Fortunately, we can get that index path using a function of UITableview
called indexPath(for:)
. Using that function, along with our code from cellForRow
we have:
// 1. check sender for the cell that was tapped
if let tappedMovieCell: MovieTableViewCell = sender as? MovieTableViewCell {
// 2. check for the right storyboard segue
if segue.identifier == "MovieDetailViewSegue" {
// 3. get reference to the destination view controller
let movieDetailViewController: MovieDetailViewController = segue.destination as! MovieDetailViewController
// 4. get our cell's indexPath
let cellIndexPath: IndexPath = self.tableView.indexPath(for: tappedMovieCell)!
// 5. get our cell's Movie
guard let genre = Genre.init(rawValue: cellIndexPath.section),
let data = byGenre(genre) else {
return
}
// 6. set the destionation's selectedMovie property
let selectedMovie: Movie = data[cellIndexPath.row]
movieDetailViewController.selectedMovie = selectedMovie
}
}
Now that the destination MovieDetailViewController
has its Movie
object reference, let's populate the labels:
movieDetailViewController.moviePosterImageView.image = UIImage(named: selectedMovie.poster)
movieDetailViewController.genreLabel.text = "Genre: " + selectedMovie.genre.capitalized
movieDetailViewController.locationLabel.text = "Locations: " + selectedMovie.locations.joined(separator: ", ")
movieDetailViewController.summaryFullTextLabel.text = selectedMovie.summary
Go ahead and run the poject at this point to see if the data gets passed along properly...
fatal error: unexpectedly found nil while unwrapping an Optional value
Discuss & Debug: Why are we getting force unwrapping errors?
Let's move our code into MovieDetailViewController
in a new function, updateViews(for:)
:
internal func updateViews(for movie: Movie) {
self.moviePosterImageView.image = UIImage(named: movie.poster)!
self.genreLabel.text = "Genre: " + movie.genre.capitalized
self.locationLabel.text = "Locations: " + movie.locations.joined(separator: ", ")
self.summaryFullTextLabel.text = movie.summary
}
And lastly, let's call updateViews(for:)
in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.updateViews(for: self.selectedMovie)
}
And re-run the project now. You should see
You may have noticed that we didn't add in a label for Movie.cast
.
What we'd like to do is be able to present a view controller of just the Actor
s for each movie when you tap on an accessory view of the MovieTableviewCell
.
To acheive that goal, here are a few pointers:
- Create a new
UIViewController
sublcass calledMovieCastDetailViewController
- Drag a
UIViewController
into storyboard, and change its custom class toMovieCastDetailViewController
- Add two labels to this view with the following details (screen shot):
castTitleLabel
:Roboto - Bold, 24pt
, Number of Lines = 1,8pt
margins to top, left, right.Vertical Content Hugging - 1000
castListLabel
:Roboto - Regular, 18pt
, Number of Lines = 0,8pt
top margin tocastTitleLabel
,24pt
left margin,8pt
right margin.
- Create a segue between
MovieTableviewCell
andMovieCastDetailViewController
, though instead of chosing a segue of type "Selection Segue" you'll be using "Accessory Action". Give the segue and identifier ofMovieCastDetailSegue
- Creating the segue of type "Accessory Action" should automatically add a "Disclosure Indicator" accesory view to the
MovieTableviewCell
, but be sure to switch it to "Detail" - Storyboard will look something like this:
- Update your code in
MovieTableViewController.prepare(for:sender:)
to recognize the new segue identifier - Populate your
castListLabel
with theActor
names so that your final product looks like
- Update the title of the
UINavigationController
ofMovieDetailViewController
to have the name of theMovie
being viewed - Same as the above, but for the
MovieCastDetailViewController
- You may have noticed that some of the labels get hidden when viewing the project on an iPhone 5s. Swap the
summaryText
UILabel
with aUITextField
to allow for this portion of the text to be scrollable.