Now that the Apple TV has opened its doors to 3rd party developers, we have a new platform for users to interact with our apps. Apteligent has updated its SDK to support tvOS to let you monitor the performance of your app on this new platform. This tutorial is designed to give you a complete introduction to building your first Apple TV app.

Project Introduction

We will be creating a dashboard that shows us some information about how our production app has been performing recently. You can use this as an introduction to tvOS development in general or leverage these tools to create dashboards of your own. The information is pulled from the Apteligent APIs for app performance (crashes & exceptions), daily active user counts, and carrier breakdown.

By the end of this tutorial, here’s what your tvOS app will look like:

It’s a simple dashboard with a few charts on the main view controller. There is also a settings view controller that allows the user to configure which app’s data is displayed.

Prerequisites

This tutorial is written in Swift and uses the ios-charts library. Before we start, you should have a background in iOS. The programming we’ll step through is light enough such that Swift knowledge isn’t a must, but a nice-to-have.

You can start from scratch and follow the tutorial in its entirety, or you can jump ahead and download the completed project here.

Set up the project

Open Xcode and create a new tvOS project. We’ll use a Single View Application for this demo and select Swift as our language. This will configure the app for deployment on tvOS.

Note: tvOS apps require tvOS 9.0 or later and bitcode settings enabled.

Next, add the ios-charts library to your new project using instructions found in the README here. This library will be used to display beautiful charts without us having to write all the code!

To keep the tutorial code clean, some of the necessary functions won’t be covered. To add these functions, copy and paste from the block below and insert them into your ViewController class.

// called after the lineChart object is initialized,
// do some styling to make it look good!
func styleLineChart(chart:LineChartView) {// set the label displayed when no data exists
chart.noDataText = “Data not available”// format the X-Axis
chart.xAxis.labelPosition = ChartXAxis.XAxisLabelPosition.Bottom
chart.xAxis.labelFont = NSUIFont(name: “HelveticaNeue-Light”, size: 14.0)!
chart.xAxis.labelTextColor = UIColor.whiteColor()
chart.xAxis.drawAxisLineEnabled = false
chart.xAxis.drawGridLinesEnabled = false

// format the Y-Axis
chart.leftAxis.labelFont = NSUIFont(name: “HelveticaNeue-Light”, size: 14.0)!
chart.leftAxis.labelTextColor = UIColor.whiteColor()
chart.leftAxis.drawAxisLineEnabled = false
chart.rightAxis.drawAxisLineEnabled = false

// format the legend
chart.legend.form = ChartLegend.ChartLegendForm.Line
chart.legend.font = NSUIFont(name: “HelveticaNeue-Light”, size: 14.0)!
chart.legend.textColor = UIColor.whiteColor()
chart.legend.position = ChartLegend.ChartLegendPosition.BelowChartCenter
}

// called after the pieChart object is initialized,
// do some styling to make it look good!
func stylePieChart(chart:PieChartView) {

// set the label displayed when no data exists
chart.noDataText = “Data not available”

// turn off the legend
chart.legend.enabled = false

// make it donut-shaped and styled
chart.holeRadiusPercent = 0.3
chart.transparentCircleRadiusPercent = 0.35
chart.holeColor = UIColor.blackColor()
chart.transparentCircleColor = UIColor.blackColor().colorWithAlphaComponent(0.6)
}

// formats a specific line in the line charts (a single data set)
func formatDataSet(inout dataSet:LineChartDataSet, color:UIColor) {

// set the color
dataSet.setColor(color)
dataSet.setCircleColor(color)

// we don’t want a circle, just a continuous line
dataSet.drawCircleHoleEnabled = false
dataSet.lineWidth = 5.0
dataSet.circleRadius = 0

// don’t show the numeric values on each data point
dataSet.drawValuesEnabled = false
}

// format the pie chart
func formatPieData(inout dataSet:PieChartDataSet) {

// spread things out a bit
dataSet.sliceSpace = 2.0

// don’t show the numeric values on each data slice
dataSet.drawValuesEnabled = false

// set the colors for the slices
dataSet.colors = [
UIColor(red: 72/255, green: 186/255, blue: 175/255, alpha: 255/255),
UIColor(red: 237/255, green: 72/255, blue: 67/255, alpha: 255/255),
UIColor(red: 198/255, green: 96/255, blue: 187/255, alpha: 255/255),
UIColor(red: 66/255, green: 199/255, blue: 111/255, alpha: 255/255),
UIColor(red: 67/255, green: 163/255, blue: 237/255, alpha: 255/255),
UIColor.orangeColor(),
]
}

// parse the date string received from the API into something friendlier
func formatDateString(dateString:String, format:String) -> String {

// create a date formatter that will parse the initial string
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = format

// get the NSDate object based on the initial string
let date:NSDate = dateFormatter.dateFromString(dateString)!

// override the dateFormatter with our desired output
dateFormatter.dateFormat = “MM/dd”

// return a pretty string from the initial date
return dateFormatter.stringFromDate(date)
}

// take a data point from the API as input (bucket), and the
// proper json keys, plus the input date format from the API =>
// return a tuple with the data point value (y-value) and the
// formatted date string (x-value)
func parseBucket(bucket:AnyObject, valueKey:String, dateKey:String, dateFormat:String) -> (value:Int, dateString:String) {

// convert the data to an NSDictionary
let bucketData:NSDictionary = bucket as AnyObject! as! NSDictionary

// get the y-value from the data set
let value:Int = bucketData[valueKey] as AnyObject! as! Int

// get the x-value (bucket date) from the JSON
let dateString:String = bucketData[dateKey] as AnyObject! as! String

// parse it and make it readable
let formattedString:String = self.formatDateString(dateString, format: dateFormat)

// return the tuple with both y and x values respectively
return (value:value, dateString:formattedString)
}

// make the HTTP request to the API. The request is asynchronous and calls
// the callback function on completion
func getData(url:String, method:String, params:AnyObject?, callback:(json:AnyObject) -> Void) {

// get the access token from the settings. This is either
// set by the user or the default from AppDelegate is used
let accessToken:String = NSUserDefaults.standardUserDefaults().objectForKey(“accessToken”) as! String

// set up the NSURLSession with the access token
let sessionConfig:NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
sessionConfig.HTTPAdditionalHeaders = [“Authorization”: “Bearer ” + accessToken]
let session = NSURLSession(configuration:sessionConfig)

// create the HTTP request
let request = NSMutableURLRequest(URL:NSURL(string:url)!)
request.HTTPMethod = method

// if it’s a POST method, set the proper HTTP headers
if(method == “POST”) {
request.addValue(“application/json”, forHTTPHeaderField: “Content-Type”)
request.addValue(“application/json”, forHTTPHeaderField: “Accept”)
}

// if parameters should be passed into the API
if(params != nil) {

// set the HTTP body with the parameters JSON-ified
do {
try request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params!, options:NSJSONWritingOptions.init(rawValue: 0))
} catch { print(“Invalid params”, error) }
}

// make the asynchronous request
session.dataTaskWithRequest(request) { (data, response, error) -> Void in

do {
// format the response data as JSON
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: [])

// we want to make sure this is run on the proper thread
dispatch_async(dispatch_get_main_queue(), {

// call back to the ViewController function. that callback
// should render the data
callback(json:json)
})

} catch {
print(“Error”, error)
}
}.resume()
}

Note: Feel free to check out how these functions work — they fully commented to explain each method, but we won’t go over them in this tutorial.

Finally, add a new file to your project named SettingsViewController.swift that subclasses UIViewController. Leave it there for now, we’ll come back to it in a bit.

Dashboard User Interface

Open up the project’s storyboard. You will notice some similarities to iOS – except the screens are bigger!

Tip: You cannot add/edit elements without zooming to 100% so it can be a pain managing larger screen sizes. You can change the perceived size of each view controller in the Attributes Inspector by setting the Simulated Metrics to Freeform. This will keep you honest with your view constraints (which you should be doing anyway to accommodate multiple form factors). It is a good way to make the views more manageable in the builder.

Add a UINavigationController and replace its default child controller (a UITableViewController) with a standard UIViewController. Make the UINavigationController the Initial View Controller so your storyboard looks something like this:

To create the dashboard UI, modify the ViewController. Customize the logo and background as you wish, but at a minimum be sure to add the labeled components as shown:

The UIViews will hold our charts, and for that we use a custom class. Click on each of the views and modify the Class and Module within the respective Identity Inspector views as shown:

This specifies the class that will render each view, making class methods and variables available to the view itself.

Dashboard Code

Now that our Dashboard UI is created, let’s move to the code. Open ViewController.swift and add the following class variables to represent the UI elements we just created:

@IBOutlet weak var dauChart: LineChartView!
@IBOutlet weak var exceptionChart: LineChartView!
@IBOutlet weak var carrierChart: PieChartView!
@IBOutlet weak var timeLabel: UILabel!

Open up your storyboard and connect the elements to the ViewController using outlets as shown:

Repeat this process for each of the variables you just created with their corresponding elements:

Add two more class variables to hold some of our chart data:

var exceptionData:NSArray = []
var crashData:NSArray = []

Finally, add the following code to your ViewController’s viewDidLoad() method:

self.styleLineChart(self.dauChart)
self.styleLineChart(self.exceptionChart)
self.stylePieChart(self.carrierChart)

This code does some added initialization to our chart views to display a specific style. Check out the additional functions you inserted earlier to see how they work or to customize the style yourself.

Pulling the data

We will populate the charts with data from the Apteligent APIs. Use the API docs to extend the functionality however you choose.

Create a new method within the ViewController class called updateData() as shown:

func updateData() {
  let appID:String = NSUserDefaults.standardUserDefaults().objectForKey("appID")

  self.getData("https://developers.crittercism.com:443/v1.0/"+appID+"/trends/dau", method: "GET", params: nil) { (json) -> Void in
    self.drawDAUChart(json as! NSDictionary)
  }
}

This will call one of our helper functions to retrieve the JSON from the given API endpoint. In this case, the endpoint is /trends/dau, which gathers the number of daily active users (DAUs) for each of the last 30 days.

Note: The appID is pulled from the app preferences, which are initially set to an Apteligent demo app. We’ll show how to customize this to your own app later.

Once retrieved, the method calls self.drawDAUChart() and passes in the result so let’s create that method by adding the code below to the ViewController class:

func drawDAUChart(jsonDict: NSDictionary) {

  // some holders for our data as we parse and sort it
  var dataEntries: [ChartDataEntry] = [] // y-values
  var dates: [String] = [] // x-values

  // extract the buckets from the JSON
  let series:NSDictionary = jsonDict["series"] as AnyObject! as! NSDictionary
  let buckets:NSArray = series["buckets"] as AnyObject! as! NSArray

  // iterate through the buckets
  for i in 0..<buckets.count {

    // parse the json item. returns a tuple of the form
    // (value, dateString) where value == dau count
    // and dateString == the date of the given dau count.
    // think of them as y and x values respectively
    let parsedData = self.parseBucket(buckets[i], valueKey: "value", dateKey: "start", dateFormat: "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ")

    // create an object for the charts library to process the info.
    // each dataEntry represents an x-y point on the chart
    let dataEntry = ChartDataEntry(value: Double(parsedData.value), xIndex: i)
    dataEntries.append(dataEntry)

    // add the x-value to the data set
    dates.append(parsedData.dateString)
  }

  // create the final object for the chart library to use
  var chartDataSet = LineChartDataSet(yVals: dataEntries, label: "Active Users")

  // do some styling and formatting of the chart
  let dataColor = UIColor(red: 72/255, green: 186/255, blue: 175/255, alpha: 255/255)
  self.formatDataSet(&chartDataSet, color: dataColor)

  // set the chart data to our newly created data series, which will draw
  // the chart itself using the passed-in data
  dauChart.data = LineChartData(xVals: dates, dataSet: chartDataSet)
}

This method extracts data from the API response, which looks something like this:

The date is parsed for each ‘bucket’ of data (in this case, a bucket is a day) and added to an array. The dates array holds each day represented as a string, these will be used as labels by our chart (think of it as the x-value). The dataEntries will be the y-value and will hold the DAU count.

var dataEntries: [ChartDataEntry] = []
var dates: [String] = []
...
for i in 0..<buckets.count {
  ...
  dataEntries.append(dataEntry)
  dates.append(newDateString)
  ...
}

The chart data is then prepared with a class from the ios-charts library, LineChartDataSet, which is then given some display options.

let chartDataSet = LineChartDataSet(yVals: dataEntries, label: "Active Users")

Finally, the x and y values are combined in a LineChartDataSet object and we assign the data object to the dauChart for rendering.

dauChart.data = LineChartData(xVals: dates, dataSet: chartDataSet)

Now that we have data being pulled from the API, parsed and sent to the chart for display, let’s make sure the method will be called and give it a run!

Add the following code to your viewDidLoad() method to initiate our data pull:

self.updateData()

Now build & run the app to see it in action!

Looks a bit lonely, doesn’t it? We’re going to replicate the same process for the other two charts with some slight differences. First, extend the updateData() method with the following code:

self.getData("https://developers.crittercism.com:443/v1.0/app/"+appID+"/crash/counts", method: "GET", params: nil) { (json) -> Void in
  self.crashData = json as! NSArray
  self.updateErrorChart()
}

self.getData("https://developers.crittercism.com:443/v1.0/app/"+appID+"/exception/counts", method: "GET", params: nil) { (json) -> Void in
  self.exceptionData = json as! NSArray
  self.updateErrorChart()
}

let pmParams:Dictionary = [ "params": [ "appId": appID, "graph": "volume", "duration":60, "groupBy":"carrier" ]]

self.getData("https://developers.crittercism.com:443/v1.0/performanceManagement/pie", method: "POST", params: pmParams, callback: { (json) -> Void in
  self.drawCarrierChart(json as! NSDictionary)
})

These are grabbing data from a few other endpoints and parsing them, just like we did with the DAU count.

Crash and exception counts are displayed in the same chart, so both of those call self.updateErrorChart() when the API call is complete. They also store the JSON as a class variable, so it can be accessed at any time. This is done so the chart can be updated as soon as either data set is loaded, without having to rely on one another.

The third chart is a pie chart that displays carrier breakdown for our app’s users, which requires a few parameters.

Now that we have the data, we need to add the chart drawing functions just as we did with the DAU count. Add these functions to your ViewController class:

// parses the data received from the exception & crash
// APIs and renders the data in the exceptionChart view
func updateErrorChart() {

  // some holders for our data as we parse and sort it
  var exceptionDataEntries: [ChartDataEntry] = [] // exception y-values
  var crashDataEntries: [ChartDataEntry] = [] // crash y-values
  var dates: [String] = [] // both x-values
  var dataSets: [LineChartDataSet] = [] // holds each series (this chart shows 2)

  // if we have exception data, add it to the set.
  // remember these are loaded asynchronously so you may
  // have crashData but no exceptionData
  if(self.exceptionData.count > 0) {

    // iterate each object in the json
    for i in 00..<self.exceptionData.count {

      // parse the json item. returns a tuple of the form
      // (value, dateString) where value == exception count
      // and dateString == the date of the given exception count.
      // think of them as y and x values respectively
      let parsedData = self.parseBucket(self.exceptionData[i], valueKey: "value", dateKey: "date", dateFormat: "yyyy'-'MM'-'dd'")

      // create an object for the charts library to process the info.
      // each dataEntry represents an x-y point on the chart
      let dataEntry = ChartDataEntry(value: Double(parsedData.value), xIndex: i)

      // if the date doesn't exist in the dates array, add it.
      // we don't want duplicated (which is possible if both exception
      // and crash data has already been loaded)
      if !dates.contains(parsedData.dateString) {
        dates.append(parsedData.dateString)
      }

      // add this data point to the current set
      exceptionDataEntries.append(dataEntry)
    }

    // create the final object for the chart library to use
    var exceptionSet = LineChartDataSet(yVals: exceptionDataEntries, label: "Exceptions")

    // do some styling and formatting of the chart
    self.formatDataSet(&exceptionSet, color: UIColor.orangeColor())

    // add the data set to the chart (this will be the complete exception set,
    // or one full line on the chart)
    dataSets.append(exceptionSet)
  }

  // if we have crash data, add it to the set.
  // remember these are loaded asynchronously so you may
  // have exceptionData but no crashData
  if(self.crashData.count > 0) {

    // iterate each object in the json
    for i in 00..<self.crashData.count {

      // parse the json item. returns a tuple of the form
      // (value, dateString) where value == crash count
      // and dateString == the date of the given crash count.
      // think of them as y and x values respectively
      let parsedData = self.parseBucket(self.crashData[i], valueKey: "value", dateKey: "date", dateFormat: "yyyy'-'MM'-'dd'")

      // create an object for the charts library to process the info.
      // each dataEntry represents an x-y point on the chart
      let dataEntry = ChartDataEntry(value: Double(parsedData.value), xIndex: i)

      // if the date doesn't exist in the dates array, add it.
      // we don't want duplicated (which is possible if both exception
      // and crash data has already been loaded)
      if !dates.contains(parsedData.dateString) {
        dates.append(parsedData.dateString)
      }

      // add this data point to the current set
      crashDataEntries.append(dataEntry)
    }

    // create the final object for the chart library to use
    var crashSet = LineChartDataSet(yVals: crashDataEntries, label: "Crashes")

    // do some styling and formatting of the chart
    let setColor = UIColor(red: 237/255, green: 72/255, blue: 67/255, alpha: 255/255)
    self.formatDataSet(&crashSet, color: setColor)

    // add the data set to the chart (this will be the complete exception set,
    // or one full line on the chart)
    dataSets.append(crashSet)
  }

  // set the chart data to our newly created data series, which will draw
  // the chart itself using the passed-in data
  self.exceptionChart.data = LineChartData(xVals: dates, dataSets: dataSets)
}

// parses the data received from the Service Monitoring
// APIs and renders the data in the carrierChart view
func drawCarrierChart(jsonDict: NSDictionary) {

  // some holders for our data as we parse and sort it
  var dataEntries: [ChartDataEntry] = []
  var carriers: [String] = []

  // extract the slices from the JSON
  let series:NSDictionary = jsonDict["data"] as AnyObject! as! NSDictionary
  let slices:NSArray = series["slices"] as AnyObject! as! NSArray

  // set up the processed data dict, which will make it easier
  // to sort and format into the chart
  var processedData:Dictionary = [ "other": 0 ]

  // iterate through each of the API slices
  for i in 0..<slices.count {

    // convert it into an NSDictionary
    let sliceData:NSDictionary = slices[i] as AnyObject! as! NSDictionary

    // extract the carrier value
    let value:Int = sliceData["value"] as AnyObject! as! Int

    // extract the carrier name
    let carrierString:String = sliceData["label"] as AnyObject! as! String

    // append this carrier as a slice to the dictionary
    processedData[carrierString] = value
  }

  // perform an in-place sort of the processed data, so the slices are
  // sorted by value, greatest to smallest
  let sortedKeys = Array(processedData.keys).sort() {
    let obj1 = processedData[$0] // get object associated w/ key 1
    let obj2 = processedData[$1] // get object associated w/ key 2

    // return the comparison (greatest to smallest)
    return obj1 > obj2
  }

  // now we want to make sure we only have a max 5 keys
  // to make sure the chart is readable and meaningful
  for i in 0..<5 {

    // if we don't have anymore keys (ex: only 3)
    // then break the loop
    if (sortedKeys.count <= i) { break } // otherwise, create a data entry with the data value let dataEntry = BarChartDataEntry(value: Double(processedData[sortedKeys[i]]!), xIndex: i) dataEntries.append(dataEntry) // add the carrier name so it's associated with the current x index carriers.append(sortedKeys[i]) } // there may be > 5 keys, so create a section that
  // encompasses 'other' carriers
  var otherValue = 0

  // if there are more than 5 keys and we need to create the other
  if(sortedKeys.count > 5) {

    // iterate through the *rest* of the keys, summing
    // their values in the 'other' slice value
    for i in 5..<sortedKeys.count {
      otherValue += processedData[sortedKeys[i]]!
    }

    // create the 'other' slice entry
    let dataEntry = BarChartDataEntry(value: Double(otherValue), xIndex: 5)
    dataEntries.append(dataEntry)

    // and add a label for this slice
    carriers.append("Other")
  }

  // create the final object for the chart library to use
  var chartDataSet = PieChartDataSet(yVals: dataEntries, label: "Carriers")

  // do some styling and formatting of the chart
  self.formatPieData(&chartDataSet)

  // set the chart data to our newly created data series, which will draw
  // the chart itself using the passed-in data
  self.carrierChart.data = PieChartData(xVals: carriers, dataSet: chartDataSet)
}

These function are so similar to the drawDAUChart() method, we won’t go through each section. Despite some minor differences, the flow is the same: parse the data, create the data set, and attach it to the chart.

Build and run your app again to see all of the charts displayed.

Note: Read through drawCarrierChart() to see the differences in how pie charts are set up and displayed. The ios-charts library provides a number of different chart types you can customize later.

Create the dashboard control layer

The data is loaded and the charts are displayed –  great! However, the data is only loaded on launch within the viewDidLoad() method. Let’s have the dashboard automatically update itself by calling the updateData() method periodically. To do that, create an NSTimer within the viewDidLoad() method as shown:

NSTimer.scheduledTimerWithTimeInterval(60*60, target:self, selector: #selector(ViewController.updateData), userInfo: nil, repeats: true)

This timer will fire every hour (60*60 seconds) indefinitely and call the ViewController.updateData() method to retrieve the latest data from the APIs and update the charts.

The timeLabel in the corner of our screen can benefit from a timer as well, as we want it to show the current time. Create a second NSTimer below the first as shown:

NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: #selector(ViewController.updateTime), userInfo: nil, repeats: true)

This timer will fire every second, and call the ViewController.updateTime() method. We want this method to simply get the current time and update the label accordingly, so add the updateTime() method to your class:

func updateTime() {
  let dateFormatter = NSDateFormatter()
  dateFormatter.timeStyle = NSDateFormatterStyle.MediumStyle
  dateFormatter.dateStyle = NSDateFormatterStyle.MediumStyle
  timeLabel.text = dateFormatter.stringFromDate(NSDate())
}

Admire the beauty

Build and run the project to see your first tvOS app in action. The data loads from the APIs asynchronously and the charts update as the information is loaded. Each hour the charts will refresh, giving you a fully-functional Apteligent dashboard.

The data displayed is pulled from the Apteligent demo app, whose API keys and App ID are hard-coded into the tutorial app. This isn’t practical for displaying info about other apps, so let’s add a settings view where the app can be customized by its users.

Settings User Interface

First, open up the storyboard and drag in a new ViewController. In its Identity Inspector, set the class to SettingsViewController to link the controller to the class we created earlier as shown:

Next, set up your view to look similar to the graphic below. At a minimum, you should have two UITextField components and a UIButton.

If you are an iOS developer, these components are familiar to you –  but you’ll probably notice a slightly different aesthetic. The UITextField items are larger, as is the default UIButton that allows us to save the data.

Settings Code

We need to connect the UI components to the code, so open up the SettingsViewController.swift file and insert the following class variables:

@IBOutlet weak var appIdField: UITextField!
@IBOutlet weak var accessTokenField: UITextField!

We also want to set up the UIButton action, so insert the following method:

@IBAction func saveSettings(sender: UIButton) {

}

Now, go back into the storyboard and connect the outlets appropriately. The saveSettings() function should be called on the UIButton’s Primary Action Triggered as shown:

The defaults for these settings are hard-coded in our AppDelegate to one of our demo app credentials, so we want to override those defaults when the user updates their settings.

The preferences are stored in NSUserDefaults, so insert the following code into the saveSettings() function:

NSUserDefaults.standardUserDefaults().setObject(self.appIdField.text, forKey: "appID")
NSUserDefaults.standardUserDefaults().setObject(self.accessTokenField.text, forKey: "accessToken")
NSUserDefaults.standardUserDefaults().synchronize()

self.navigationController?.popViewControllerAnimated(true)

When the Save Settings button is pressed, the current UITextField values are saved to the NSUserDefaults and the user is sent back to the dashboard view. Easy enough!

To show the user their current settings upon opening the settings view, insert the following viewWillAppear() function to the SettingsViewController class:

override func viewWillAppear(animated: Bool) {
  if let appID = NSUserDefaults.standardUserDefaults().objectForKey("appID") {
    self.appIdField.text = appID as? String
  }

  if let accessToken = NSUserDefaults.standardUserDefaults().objectForKey("accessToken") {
    self.accessTokenField.text = accessToken as? String
  }
}

This loads the current settings from the NSUserDefaults and displays them within the appropriate UITextField objects.

Trying the interface

Now that we have a Settings view, we want to do more than just enjoy the charts and interact with the app itself. Build and run the app to open up the simulator.

If you don’t see the TV remote on your screen, go to the menu and select Hardware > Show Apple TV Remote. You’ll see this window pop up, and if you own an Apple TV yourself, the controls will look pretty familiar.

Working the remote on the simulator takes a bit of getting used to, especially when your instinct is to just point-and-click. To simulate your finger scrolling on the trackpad, hold the option key and move the mouse over the remote. Click at any time to ‘tap’ the trackpad and make a selection.

If you click at any point while you’re viewing the main dashboard, you’ll go straight into the settings view. Why is this happening?

Well, it’s because the only element on the dashboard screen that can be selected is the settings button, so it is automatically in focus when the view is being displayed. To see this more clearly, look at the top UITextField in the settings screen. It is expanded and in focus. Scroll your mouse vertically over the trackpad while holding option to see how the element focus changes.

If you use UIKit elements in your app (e.g., standard UITextFields, UIButtons), tvOS is smart about selecting the right elements from left to right, then top to bottom as your finger scrolls across the screen. This makes for a consistent experience across tvOS apps, and it can be customized if needed. To learn more about controlling the User Interface, check out Apple’s Programming Guide on the topic.

One last note about the interface: notice there’s no back button in the settings screen as we’re used to seeing in most UINavigationController apps in iOS. That’s because the Apple TV Remote has a hardware back button. Tap the Menu button on the remote to trigger a pop in the navigation controller.

Going meta

Now that our tutorial app is fully-functioning and displays the latest Crash, Exception, DAU and Carrier information about a specified app, we want to make sure our dashboard is performing as expected.

Apteligent’s monitoring tools are available for tvOS, and can be added to any project in just a few minutes. To install the SDK, follow the guide in our documentation. You can get started with as little as one line of code to start seeing crashes, exceptions and network performance for your newly-created app. Best of all, it’s totally free to get started!

In theory, we could plug in the dashboard’s Apteligent App ID into the Settings View… to get a dashboard of all the issues happening… in the dashboard app…

That isn’t the most practical thing to do, but we do want to ensure the app is running smoothly. This means monitoring for crashes, JSON exceptions and other issues, and that’s what the Apteligent SDK helps us do!

Wrapping Up

Add your own Apteligent credentials to the dashboard app’s settings or customize the data sources and the user interface to make this app your own. We’ll be improving on this project over time, and would love to see your additions and suggestions.

We’re social on Github, so feel free to submit your updates! The code for this tutorial has been frozen at tag demo1 so it can be retrieved at any time, but keep an eye on the full project moving forward.

Happy coding, and feel free to ping us if you have any questions @apteligent.