Adding a Dynamic Top Shelf to the Photo Gallery App
The Top Shelf area of the Apple TV Home screen is a great place to provide more information about an app, as well as to showcase what an app has to offer to its users. At the end Chapter 5, you added a custom static Top Shelf image to the Photo Gallery app. In this chapter, you are going to customize the Top Shelf even further by adding a scrollable collection of dynamic thumbnail images for users to choose from, as shown in Figure 6-1.
Figure 6-1. The dynamic Top Shelf of the Photo Gallery app
Users will be able to scroll through the collection of thumbnail images to see a preview of all of the albums within the app. Selecting any of the thumbnail images will open the Photo Gallery app, taking the user to the selected full-screen photo.
Application Extensions
You will add support for a dynamic Top Shelf by adding a new TV Services application extension that implements the TVTopShelfProvider protocol to the Photo Gallery app. App extensions are not apps themselves, but instead allow apps to provide additional functionality to the rest of the system. Apple Watch apps, custom keyboards, and Today widgets are some of the other examples of app extensions, specifically for iOS.
Figure 6-2. Adding a TV Services Extension to the Photo Gallery app
Figure 6-3. Adding the Photo Gallery Extension to the Photo Gallery app
Figure 6-4. The Photo Gallery Extension has been added to the Photo Gallery project
Adding Classes and Images to the Photo Gallery Extension
Since the app extension is a separate target from the main Photo Gallery app, you first need to add the classes and images that it needs to know about to generate the dynamic Top Shelf data.
Figure 6-5. The Build Phases tab selected for the Photo Gallery Extension target
Figure 6-6. The Compile Sources Build Phase of the Photo Gallery Extension target
Figure 6-7. Adding the Gallery, Album, and Photo classes to the Photo Gallery Extension
Now that the Photo Gallery Extension knows what a Gallery, Album, and Photo are, you just need to add the actual image files to the target so that they can be used to generate the thumbnail images.
Figure 6-8. Adding the image files to the Photo Gallery Extension target
Note In case you were wondering why the image files were not added to an Asset Catalog in this instance, it is because the dynamic Top Shelf thumbnail images need to be initialized using an image file URL. Image file URLs are not available when images are packaged up in an asset catalog, so that is why you instead add them to the Photo Gallery Extension target as general bundle resources.
Implementing the TVTopShelfProvider Protocol
Now that the Photo Gallery Extension target contains the necessary classes and images needed for the dynamic Top Shelf data, it is time to generate and return that data by implementing the TVTopShelfProvider protocol in the ServiceProvider.swift file.
Select the ServiceProvider.swift file from the Project navigator and replace the default topShelfItems computed property definition with the code below:
1 var topShelfItems: [TVContentItem] {
2 let gallery = Gallery()
3
4 var albums: [TVContentItem] = []
5
6 // create a TVContentItem for each album in the gallery
7 for albumIndex in 0..<gallery.albums.count {
8 let album = gallery.albums[albumIndex]
9
10 var photos: [TVContentItem] = []
11
12 // create a TVContentItem for each photo in the album
13 for photoIndex in 0..<album.photos.count {
14 let photo = album.photos[photoIndex]
15
16 guard let photoIdentifier = TVContentIdentifier(identifier: photo.name, container: nil) else { return [] }
17 guard let photoItem = TVContentItem(contentIdentifier: photoIdentifier) else { return [] }
18
19 photoItem.title = photo.name
20 photoItem.imageURL = NSBundle.mainBundle().URLForResource(photo.name, withExtension: ".jpg")
21 photoItem.displayURL = NSURL(string: "photogallery:viewTopShelfItem?album=(albumIndex)&photo=(photoIndex)")
22
23 photos.append(photoItem)
24 }
25
26 guard let albumIdentifier = TVContentIdentifier(identifier: album.name, container: nil) else { return [] }
27 guard let albumItem = TVContentItem(contentIdentifier: albumIdentifier) else { return [] }
28
29 albumItem.title = album.name
30 albumItem.topShelfItems = photos
31
32 albums.append(albumItem)
33 }
34
35 return albums
36 }
The topShelfItems computed property is now going to return an array of TVContentItems, one for each album. Each album TVContentItem within the returned array is going to contain another array of TVContentItems, one for each photo within that album.
At the beginning of the topShelfItems computed property code block, you first create an instance of the Gallery class (Line 2) so that you can reference all of the album and photo data and information. Next, you loop over all of the albums in the gallery (Line 7) to add the associated TVContentItems to the albums array. Then, within the photos for loop, you will loop over all of the photos within that album (Line 13) to add the associated TVContentItems to the photos array.
Within the photo for loop, you first create a TVContentIdentifier using the name of the current photo (Lines 14-16), and then create the actual TVContentItem using that TVContentIdentifier (Line 17). You then set the title of the photo item to be the name of the photo (Line 19), so that it will be displayed underneath the thumbnail image when displayed in the Top Shelf, as shown in Figure 6-9.
Figure 6-9. The title of the first TVContentItem in the Animals album is Cows
After setting the title, you then set the imageURL (Line 20) to be the location of the associated image file that you copied to the Photo Gallery Extension earlier in the chapter. Finally, you set the displayURL (Line 21) to a specially formatted string that will be passed to the Photo Gallery application to identify which image thumbnail was selected from the Top Shelf. The displayURL string contains the photogallery scheme (which we will discuss later in the chapter) as well as the album and photo index values associated with the photo.
Once those three properties have been set for the current photo, the TVContentItem is added to the photos array at the end of the loop (Line 23).
After the photo loop is complete, the photos array contains all of the photo TVContentItems for the current album. Next, you create a TVContentIdentifier and TVContentItem for the album, just like you did for each photo (Lines 26-27). Then, you set the title of the TVContentItem to the album name (Line 29) so it will appear above each collection of image thumbnails, as shown in Figure 6-9.
After setting the topShelfItems property to be the photos array (Line 30), you add the completed album TVContentItem to the albums array at the end of the album loop (Line 32). Once you have added all of the albums to the albums array, the array is returned (Line 35).
Phew! That was quite a lot of code, but you got through it! Now, if you attempt to build and run the app extension, you will be presented with the dialog window shown in Figure 6-10. Because app extensions are not apps that can be run independently, you need to select Top Shelf from the list to see your changes reflected in the Top Shelf of your Apple TV.
Figure 6-10. Selecting which app to run with the Photo Gallery Extension
After clicking Run, you will see the static Top Shelf image you added in Chapter 5 replaced with the dynamic thumbnails for all of the albums and photos contained within the gallery. Swiping up on the Apple TV remote when the Photo Gallery app is selected allows you to focus on the Top Shelf items and swipe back and forth between all of the available photos.
Launching the Photo Gallery App from a Top Shelf Thumbnail Image
Browsing through all of the thumbnails is great, but what we really want to do is click on one of those thumbnails to view the selected image within the Photo Gallery app. To allow the Photo Gallery Extension to do this, you are first going to have to make some changes to the main Photo Gallery app.
Figure 6-11. Selecting the Info.plist of the Photo Gallery app
Figure 6-12. Adding the URL types item to the Info.plist
Figure 6-13. Changing the URL identifier to Photo Gallery URL
Figure 6-14. Adding the URL Schemes subkey to the Info.plist
Figure 6-15. Adding the photogallery URL Scheme to the Info.plist
Adding the photogallery URL Scheme to the Info.plist registers that scheme (as mentioned earlier in the chapter) with the system. Now, whenever any URL that begins with photogallery is opened on the Apple TV, it will be opened by the Photo Gallery app.
Build and run the app extension and select one of the thumbnails from the Top Shelf. The Photo Gallery app is now launched, but the image that was selected is not yet being displayed. To accomplish this, you first need to tell the Photo Gallery application how to handle the URL information that is passed to the app when it is opened from a Top Shelf thumbnail image.
When an Apple TV app is launched from a matching URL scheme, the URL is passed to a UIApplicationDelegate protocol method called openURL. Open the AppDelegate.swift file from the Project navigator and add the follow code to the end of the AppDelegate class declaration:
1 func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool {
2 var albumIndex: Int?
3 var photoIndex: Int?
4
5 // extract the album and photo index from the url
6 guard let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) else { return true }
7 guard let queryItems = components.queryItems else { return true }
8 for queryItem in queryItems {
9 if let valueString = queryItem.value, value = Int(valueString) {
10 if queryItem.name == "album" {
11 albumIndex = value
12 }
13 else if queryItem.name == "photo" {
14 photoIndex = value
15 }
16 }
17 }
18
19 // if the album and photo index values have been set, view that photo
20 if albumIndex != nil && photoIndex != nil {
21 // pass the album and photo index values to the TableViewController
22 if let tableViewController = window?.rootViewController as? TableViewController {
23 tableViewController.viewSelectedTopShelfPhoto(photoIndex!, inAlbum: albumIndex!)
24 }
25 }
26
27 return true
28 }
In this method, you first use the URL passed in from the Top Shelf TVContentItem to create an NSURLComponents object (Line 6) in order to extract the albumIndex and photoIndex values from the URL string (Lines 8-17). Then, if both of those values have been set (Line 20), you find the TableViewController object from the main app window (Line 22) and pass it to its viewSelectedTopShelfPhoto method (Line 23).
Completing the Photo Gallery App
Now you need to make the appropriate changes to the TableViewController class to handle this new method being called from the App Delegate. Open the TableViewController.swift file and add the following line of code to the top of the class declaration, beneath the gallery property declaration:
1 var selectedTopShelfItem: (albumIndex: Int?, photoIndex: Int?) = (nil, nil)
This defines a tuple property containing two integers, one for the album index and one for the photo index. Initially both of the index values are nil, indicating that a Top Shelf item has not been selected.
Next, define the viewSelectedTopShelfPhoto method by adding the following code to the end of the class declaration:
1 func viewSelectedTopShelfPhoto(photo: Int, inAlbum album: Int) {
2 // save the selected top shelf album photo index values
3 self.selectedTopShelfItem = (album, photo)
4
5 // if I am not the presented view controller, pop back
6 if let presentedViewController = self.presentedViewController {
7 presentedViewController.dismissViewControllerAnimated(false, completion: nil)
8 }
9 else {
10 self.checkSelectedTopShelfItem()
11 }
12 }
In this method, you first store the album and photo index values within the new selectedTopShelfItem tuple property (Line 3). Then, you check to see what the current state of the app’s view controller hierarchy is. If a user has previously left the app viewing another full-screen image (Line 6), then you would want to dismiss the presented PageViewController object (Line 7) before viewing the newly selected photo. If there is no presentedViewController set (Line 9), then that means there is no PageViewController object to dismiss, so the user can continue to view the selected photo (Line 10).
Next, add the following code to the end of the class declaration to check whether a Top Shelf item has been selected, and if so, performing the appropriate action:
1 func checkSelectedTopShelfItem() {
2 if let albumIndex = self.selectedTopShelfItem.albumIndex {
3 self.tableView.selectRowAtIndexPath(NSIndexPath(forRow: albumIndex, inSection: 0), animated: false, scrollPosition: .None)
4 self.performSegueWithIdentifier("SelectAlbumSegue", sender: nil)
5 }
6 }
In this method, if the album index within the selectedTopShelfItem is set (Line 2), you can then select that row in the table view (Line 3) and manually perform the SelectAlbumSegue that is used when a user clicks one of the albums from the list to browse the photos within it (Line 4).
When a user selects a Top Shelf image and the app is opened, depending on its previous state, a PageViewController object may need to be dismissed. If that is the case, the app needs to be notified when that process is complete so that it can continue to view the selected Top Shelf item photo. The easiest way to do this is to override the Table View Controller’s viewDidAppear method by adding the following code to the TableViewController.swift file after the viewDidLoad method declaration:
1 override func viewDidAppear(animated: Bool) {
2 super.viewDidAppear(animated)
3 self.checkSelectedTopShelfItem()
4 }
Now, whenever the Table View Controller appears, the selectedTopShelfItem is checked (Line 3).
The final change you have to make is to pass the selected photo index to the PageViewController object from within the prepareForSegue method of the Table View Controller. Add the following changes to the TableViewController.swift file so that the prepareForSegue looks like this:
1 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
2 if segue.identifier == "SelectAlbumSegue" {
3 if let pageViewController = segue.destinationViewController as? PageViewController, row = self.tableView.indexPathForSelectedRow?.row {
4 pageViewController.album = self.gallery.albums[row]
5
6 // if there is a selected photo index set it as well and then reset it
7 if let photoIndex = self.selectedTopShelfItem.photoIndex {
8 pageViewController.pageIndex = photoIndex
9 self.selectedTopShelfItem = (nil, nil)
10 }
11 }
12 }
13 }
Now, if the segue matches the SelectAlbumSegue identifier (Line 2), after a Page View Controller is created (Line 3) and its album has been initialized (Line 4), its pageIndex is set to the photoIndex of the selectedTopShelfItem (Lines 7-8) so that the appropriate image will be selected once the app is launched. Finally, the selectedTopShelfItem is reset back to its uninitialized state (Line 9).
Now, when you select a thumbnail image from the Top Shelf, the Photo Gallery app is launched and the appropriate album is displayed with the appropriate photo already selected.
Summary
Congratulations! Over the course of the past few chapters, you have created a Photo Gallery app that enables users to select and view photos from within a number of different photo albums, in addition to viewing those photos when selecting them from the Top Shelf area of the Apple TV Home screen.
You have learned about using Page View Controllers, Table View Controllers, and Application Extensions on tvOS, all using Swift. The knowledge you have gained throughout these chapters will provide you with a great foundation for developing other apps in the future using these aspects of tvOS development.
In the next chapter, we will explore how to store app information on the Apple TV itself, as well as how to store and sync data to the cloud.
Exercises