rukiNyc / AC3.2-Tableviews_Part_3

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

C4Q Movie Reel

(AC3.2-Tableviews_Part_3: Tableview Segues)


Time to Reel it In

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:

  1. 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.


Goals

  1. Create a new custom UIViewController to display a single Movie object's data
  2. Understand segues in storyboard to transition between view controllers
  3. Better understanding of the delegate pattern in programming.

Readings

  1. General Reference on Xcode (very useful)
  2. Configuring a Segue in Storyboard - Apple
  3. Using Segues (lots of great info here)
  4. Navigation Controller Implementation - tuts+ (helpful reference and example)

1. Adding a new UIViewController to Display a Movie

Storyboard Changes (Use iPhone 6s for your simulation)

  1. Drag in a UIViewController into Main.storyboard from the Objects Library in the Utilities Pane and place it next to the MovieTableViewController
  2. Select the prototype cell (make sure you have the actual MovieTableViewCell selected) and Control-Drag to the new UIViewController
  3. 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.
  1. Select the segue object in storyboard (the -> arrow), and in the Attribute Inspector, set it's identifier to MovieDetailViewSegue
  2. 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 to Aspect Fit
  1. Drag in 4 UILabels below the UIImageView in a vertical row
  • Label them (in order): Genre, Location, Summary and Summary Text
  • Set their fonts to Roboto Regular - 17pt, except for Summary Text which will be Roboto-Light - 14pt
  • Set the number of lines for Summary Text to 0
  1. Select all of the UILabels 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 of Summary Text to 1000
  1. Your view controller should resemble:

Storyboard of MovieDetailViewController

Being able to set constraints in this batching form is one of the nice advantages of using storyboards.

Storyboard linking

  1. Add a new file named MovieDetailViewController that subclasses UIViewController and place it in the correct folder and Xcode group
  2. In Main.storyboard change the custom class of the view controller we just added to be MovieDetailViewController
  3. Create outlets for each of the labels and the imageView. Name them:
  • moviePosterImageView, genreLabel, locationLabel, summaryLabel, and summaryFullTextLabel
  1. Additionally, give MovieDetailViewController an instance variable of type Movie
  • internal var selectedMovie: Movie!
  • This property will hold a reference to the Movie from the cell that was tapped.

Preparing for Segue

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?

Updating MovieDetailViewController with a Movie

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

Detail View Controller Full


2. Exercise

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 Actors for each movie when you tap on an accessory view of the MovieTableviewCell.

To acheive that goal, here are a few pointers:

  1. Create a new UIViewController sublcass called MovieCastDetailViewController
  2. Drag a UIViewController into storyboard, and change its custom class to MovieCastDetailViewController
  3. 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 to castTitleLabel, 24pt left margin, 8pt right margin.
  • Cast List VC Storyboard
  1. Create a segue between MovieTableviewCell and MovieCastDetailViewController, though instead of chosing a segue of type "Selection Segue" you'll be using "Accessory Action". Give the segue and identifier of MovieCastDetailSegue
  • 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:
  • Full Storyboard for Exercise 2
  1. Update your code in MovieTableViewController.prepare(for:sender:) to recognize the new segue identifier
  2. Populate your castListLabel with the Actor names so that your final product looks like Table View Controller w/ Detail Accessory Cast List Detail VC

Other Optional Exercises

  1. Update the title of the UINavigationController of MovieDetailViewController to have the name of the Movie being viewed
  2. Same as the above, but for the MovieCastDetailViewController
  3. You may have noticed that some of the labels get hidden when viewing the project on an iPhone 5s. Swap the summaryText UILabel with a UITextField to allow for this portion of the text to be scrollable.

About

License:MIT License


Languages

Language:Swift 100.0%