Contents


Overview

Skill Level: Intermediate

This tutorial assumes a basic familiarity with Xcode app development.

The following demonstrates how to setup a tvOS app, create a compelling UI, and connect it to dynamic data from Twitter.
Originally create for the IBM Stadium Experience and adapted for this tutorial. iOS experience will help, but is not required.

Ingredients

  • macOS
  • Xcode 8
  • Swift 3.0
  • Twitter account

Step-by-step

  1. Create a Full-Screen UICollectionView

    We will now create the biggest UICollectionView you have ever had to make. To start, you should create a tvOS Xcode project by opening Xcode (I’m using Xcode 8), selecting new project, and selecting the tvOS single-view app option.

    New project screen

    From here, we can go directly into Main.storyboard. There will be one TV-sized view controller created already, so using interface builder, drag a collection view onto that view controller. Since we want the collection view to fill the whole screen, apply constraints to do just that. Something like this:

    collectionViewConstraints

    For this app, we want to create a 10 x 6 tiled grid, meaning ten cells should fit the width and six cells should fit the height of the collection view. Dealing with a 1920 x 1080 TV, we can make our cells 192 x 180. While viewing the size inspector for your collection view, enter those values into the cell size fields. Continue on by making Min Spacing and Section Insets all zero.

    Next, on the attributes inspector for your collection view, ensure the flow is set to “Horizontal.” Since displaying a lot of images is our goal, we need to add an image view to the cell in storyboard. Once added, make it the same size as the cell by applying constraints to fill the entire cell. You should set a sample image on the image view, with the view mode set to Aspect Fill and the Clip Subviews box checked to better visualize your progress. We need to make sure to set the cell identifier while still in storyboard, like so:

    cellID

    Using the assistant editor, we will create an IBOutlet for our collection view within the ViewController.swift file:

    collectionViewOutlet

  2. Create UICollectionViewCell

    Because of the image on our cell, we need a custom UICollectionViewCell subclass. Therefore, create a Swift file called StadiumCollectionViewCell with the following contents:

    import UIKit

    class StadiumCollectionViewCell: UICollectionViewCell {

    }

    In storyboard, assign the class to your cell:

    cellClass

    Lastly, create an IBOutlet from the image within your collection view cell. Your StadiumCollectionViewCell class should now include the following:

    @IBOutlet weak var stadiumImageView: UIImageView!
  3. Connect UI to the Code

    In the ViewController.swift, we are going to set up the UICollectionView Delegate and Datasource, so you should adopt those protocols:

    class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {



    override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.delegate = self
    collectionView.dataSource = self
    }

    With that setup out of the way, we need to provide some datasource methods:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 60
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "stadiumCell", for: indexPath) as? StadiumCollectionViewCell {
            return cell
      }
        return UICollectionViewCell()
    }

    A lot of what we did is essentially iOS boilerplate for a collection view, but if you run the app now, you should see a full TV screen of collection view cells.

    60cells

  4. Setup Twitter Data Handlers

    Now, since we want a dynamic collection view of images, it is time to create a Twitter development app at https://apps.twitter.com. While this specific process may change in the future, I trust it won’t be too difficult. The reason for creating a Twitter dev app is so we can pull dynamic data from Twitter, giving our app fresh content every time you use the tvOS app.

    1. Once you’ve navigated to https://apps.twitter.com, click “Create New App.”
    2. Fill out the required fields and agree to Twitter’s developer agreement.
    3. Now your app is created and you should have access to all the credentials you need to get data from the Twitter API.
    4. Before we finish, you will need to find your consumer key and consumer secret, located under the “Keys and Access Tokens” tab.
    5. Once found, copy into a plist within your Xcode project. Additionally, you will need to use an authentication URL and a URL to get the right images from Twitter, you can copy those from here. Your plist should look similar to the following, with your own secret and key of course: 

    twitterPlist

    Luckily, with some helpful references, I was able to build a Twitter data manager that authenticates our app. Here is the code for that:

     

    import UIKit
    /// Handles the calls to the Twitter API
    class TwitterDataManager: NSObject {
        
        var twitterConsumerKey = ""
        var twitterConsumerSecret = ""
        var twitterAuthURL = ""
        var mediaWallURL = ""
      
        static let sharedInstance = TwitterDataManager()
        fileprivate override init() {
            
            if let path = Bundle.main.path(forResource: "TwitterInfo", ofType: "plist") {
                if let myDictionary = NSDictionary(contentsOfFile: path) {
                    
                    if let key = myDictionary["twitterConsumerKey"] as? String, let secret = myDictionary["twitterConsumerSecret"] as? String,
                        let authURL = myDictionary["twitterAuthURL"] as? String, let mediaURL = myDictionary["mediaWallURL"] as? String {
                        
                            self.twitterConsumerKey = key
                            self.twitterConsumerSecret = secret
                            self.twitterAuthURL = authURL
                            self.mediaWallURL = mediaURL
                    }
                }
            }
            
        }
        
        // MARK: App only Auth tasks for Twitter
        // extracted from https://github.com/rshankras/SwiftDemo
        
        func getBearerToken(_ completion: @escaping (_ bearerToken: String) ->Void) {
            var request = URLRequest(url: URL(string: twitterAuthURL)!)
            
            request.httpMethod = "POST"
            request.addValue("Basic " + getBase64EncodeString(), forHTTPHeaderField: "Authorization")
            request.addValue("application/x-www-form-urlencoded;charset=UTF-8", forHTTPHeaderField: "Content-Type")
            let grantType =  "grant_type=client_credentials"
            
            request.httpBody = grantType.data(using: String.Encoding.utf8, allowLossyConversion: true)
    
            URLSession.shared.dataTask(with: request) { data, response, error in
                
                guard let authData = data else {
                    print(error?.localizedDescription)
                    return
                }
                
                do {
                    let results: NSDictionary = try JSONSerialization.jsonObject(with: authData, options: JSONSerialization.ReadingOptions.allowFragments) as! NSDictionary
                    if let token = results["access_token"] as? String {
                        completion(token)
                    } else {
                        print(results["errors"])
                    }
                } catch let error as NSError {
                    print(error.localizedDescription)
                }
            }.resume()
            
        }
        
        func getBase64EncodeString() -> String {
            
            let consumerKeyRFC1738 = twitterConsumerKey.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)
            let consumerSecretRFC1738 = twitterConsumerSecret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)
            
            let concatenateKeyAndSecret = consumerKeyRFC1738! + ":" + consumerSecretRFC1738!
            
            let secretAndKeyData = concatenateKeyAndSecret.data(using: String.Encoding.ascii, allowLossyConversion: true)
            
            let base64EncodeKeyAndSecret = secretAndKeyData?.base64EncodedString(options: NSData.Base64EncodingOptions())
            
            return base64EncodeKeyAndSecret!
        }
    
    }
    

    I encourage you to take the time to understand this code, but for the most part, it is standard iOS Oauth code. For more information on logging in with Twitter, go here.

  5. Pull and Format Twitter Data

    In order to get all the images we need for our app, we will need to do a few data calls. The first function here authenticates our app, retrieves Twitter data based on the mediaURL we have in our plist, and sends that data to another method as a dictionary. We call this method from our viewDidLoad() method:

    authenticateRequest(TwitterDataManager.sharedInstance.mediaWallURL)

    Here is the method definition:

    func authenticateRequest(_ url:String) {
    
            // if using twitter, use this method. Add more auth methods if necessary, later
            TwitterDataManager.sharedInstance.getBearerToken { bearerToken in
              
                var request = URLRequest(url: URL(string: url)!)
                request.httpMethod = "GET"
              
                let token = "Bearer " + bearerToken
                request.addValue(token, forHTTPHeaderField: "Authorization")
                
                URLSession.shared.dataTask(with: request) { data, response, error in
                    if let validData = data , error == nil {
                        do {
                            let results = try JSONSerialization.jsonObject(with: validData, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : AnyObject]
                            if let resultDictionary = results {
                                self.processMediaWall(resultDictionary)
                            }
                            
                        } catch let error as NSError {
                            print(error.localizedDescription)
                        }
                    } else {
                        print(error?.localizedDescription)
                    }
                }.resume()
            }
    
        }
    

    Now we need to massage that data so our collection view can handle it.

    func processMediaWall(_ results: [String : AnyObject]) {
    
            var mediaURLs = [String]()
            if let statuses = results["statuses"] as? [AnyObject] {
                
                // 1 extract image urls from all the twitter data
                for status in statuses {
                    
                    // unrap all the things, to get to a mediaURL of an image
                    if let entities = status["entities"] as? [String : AnyObject], let mediaArray = entities["media"] as? [AnyObject],
                        let media = mediaArray.first as? [String : AnyObject], let mediaURL = media["media_url"] as? String {
                        
                        mediaURLs.append(mediaURL + ":small")
                    }
                }
                
                // 2 repeat images if less than 120
                if mediaURLs.count < 120 {
                    
                    let tempArray = mediaURLs
                    // repeat loop until we have enough media URLs
                    parentLoop: while mediaURLs.count < 120 {
                        childLoop: for url in tempArray {
                            
                            mediaURLs.append(url)
                            if mediaURLs.count == 120 {
                                break parentLoop
                            }
                        }
                    }
                }
                
                // 3 pull images from server using URLs from Twitter
                var images = [UIImage]()
                for url in mediaURLs {
                    ImageLoader.sharedLoader.imageForUrl(url, completionHandler: { (image, url) -> () in
                        if let remoteImage = image {
                            images.append(remoteImage)
                            self.populateMediaWall(images)
                        }
                    })
                }
            }
    
        }
    

    There are three different sections in the code above. The first simply extracts the image URL for each Twitter post received. The second part ensures we have enough images to fill two TV screens, and if we don’t, we reuse images. A big reason we might have to reuse images is because Twitter’s API will only show recently tweeted images, limiting our pool of data. The last piece of code here actually grabs the image binary from Twitter’s servers. In our code, we use a very simple helper library called ImageLoader. I recommend you get that code here and place it into your Xcode project.

  6. Connect Twitter Data to UI

    From the processMediaWall method, we call the populateMediaWall method where we load the images into our collection view. The code looks like this:

    func populateMediaWall(_ images: [UIImage]) {
    
        wallImages = images
            
        DispatchQueue.main.async {
            self.collectionView.reloadData()
        }
    }
    

    This code also ensures we are populating our collection view on the main thread, where all the UI lives. We are making progress, but this won’t work just yet, we need to add an UIImage array to our ViewController class so we have a record of images we’ve received from the server.

    var wallImages = [UIImage]()

    Secondly, we need to return the count of that array in our collection view numberOfItemsInSection method. Then, in the cellForItemAtIndexPath method, we need to set one of the wallImages onto our cell.stadiumImageView. Those methods should now look like this:

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return combinedImages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "stadiumCell", for: indexPath) as? StadiumCollectionViewCell {
            cell.stadiumImageView.image = combinedImages[indexPath.row]
            return cell
        }
        return UICollectionViewCell()
    }
    

    Everything should work now, right? Still wrong, because the App Transport Security has blocked our http request because it is insecure. You can read more about ATS here, but for our test app, we are going to unblock http requests by adding the following to our info.plist:

    Now, finally, we can run our tvOS application and see a full screen of images from Twitter.

    working1

  7. Add The Infinite Scrolling Effect

    To wrap up this tutorial with a nice bow, we are going to make our media wall (seemingly) infinitely scrolling. To do this, we need to have three TV screens of images, meaning 180 images! The first 60 and the last 60, though, will be the same to make the infinite scrolling smooth. View this diagram as an example:

    3screen_diagram

    We have two unique screens and then a duplicate first screen on the end so that whenever the second version of screen 1 gets fully in view, the app instantly scrolls to the beginning of the collection view, displaying screen 1 again. This creates the illusion that our media wall just wraps around on itself, like a cylinder, when really we are just changing the collection view's x position. First thing we need to do in this process is add some instance variables to the top of ViewController:

    var combinedImages = [UIImage]()
    var scrollingPoint = CGPoint(x: 0, y: 0)
    var endingPoint = CGPoint.zero
    

    These variables will be explained more in a second, but we still need to duplicate our first screen’s data, so from populatedMediaWall, we call setupCollectionViewData which contains:

    func setupCollectionViewData() {
            
        if wallImages.count == 120 {
            // append data from first full screen to all data so we have data to fill 3 screens
            let half = wallImages.count / 2
            let screenOneData = Array(wallImages[0 ..< half])
            combinedImages = wallImages + screenOneData
        } else {
            combinedImages = wallImages
        }
            
    }
    

    Additionally, in the populatedMediaWall method we created earlier, add a call to beginSlowScroll and a reset of one of wallImages array, making our method look like so:

    func populateMediaWall(_ images: [UIImage]) {
    
        wallImages = images
        setupCollectionViewData()
            
        DispatchQueue.main.async {
            self.collectionView.reloadData()
    
            if self.wallImages.count == 120 {
                self.beginSlowScroll()
                self.wallImages.removeAll()
            }
        }
    }
    

    To finish, we add our slow scrolling methods:

    func beginSlowScroll() {
    
        endingPoint = CGPoint(x: view.frame.size.width * 2, y: 0)
        Timer.scheduledTimer(timeInterval: 0.030, target: self, selector: #selector(ViewController.scrollSlowlyToPoint), userInfo: nil, repeats: true)
    }
        
    func scrollSlowlyToPoint() {
            
        collectionView.contentOffset = scrollingPoint
    
        // Detect when scrolled to the end and scroll without animation to index 0
        if scrollingPoint.equalTo(endingPoint) {
            // reset scrolling point and instantly scroll to first cell
            scrollingPoint.x = 0
        }
            
        scrollingPoint = CGPoint(x: scrollingPoint.x + 1, y: scrollingPoint.y)
    }
    

    These methods are used so we can change the contentOffset of our collection view at a very slow rate. We use an NSTimer that calls the scrollSlowlyToPoint method to move our collection view contents one pixel over at a rate of every 0.030 seconds. Once our current scrolling point matches our ending point, we reset our collection view content to start at the beginning. If you run the app now, you should get a beautiful scrolling wall of images!

    finalScroll

    For further customization, the mediaWallURL value in your Twitter plist can be swapped out to display different content. Simply exchange the text that says “nfl” with the Twitter handle of whatever account you would like to show images of.

  8. Final Words

    I know there are a lot of steps, but the end product is a compelling tvOS app that can be displayed anywhere. I hope this provides a great example of a tvOS app integrated with the Twitter API. While the platform is different, this tutorial should be very familiar for iOS developers and is meant as an entry-point into tvOS so developers can create their own tvOS applications.

    For info on the IBM Stadium Experince, go here.

    For a copy of this Xcode project, leveraging Swift 3, go here.

    To learn more about me, the author, check out my Twitter: @tfrank64

Join The Discussion