WatchKit: Table and network fetch

After months of beta releases, the final version of Xcode 6.2 is here. Xcode 6.2 includes the SDK for the much anticipated Apple Watch. In this tutorial we will see how to create a WatchKit app that fetches data from the network and displays a simple table and a detail view. Tables in WatchKit work a little bit different than Table Views on iPhone. The full project is available on Github.

Open Xcode and create a single view project with Swift as its default language. Leave the use Core Data checkbox unchecked. Click File>New>Target and select WatchKit app as shown in the image below.

Add a WatchKit app target in your project
Add a WatchKit app target in your project

After clicking next you will see a bunch of files added to your project as well as two new targets, a WatchKit extension and a WatchKit app. In order to preserve the Watch battery we will perform the network fetching on the iPhone and then pass the data to the WatchKit App. Apple provides delegate methods for such needs. Open Appdelegate.swift file and add the following code:

  func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {
        
        if let request = userInfo["request"] as? String {
            if request == "refreshData" {
                var parser = XMLParser()
                parser.startParsing(NSURL(string: "http://www.ds.unipi.gr/category/announcements/feed/")!, completionClosure: {(articles, error) in
                    if error == nil {
                        reply(["articleData": NSKeyedArchiver.archivedDataWithRootObject(articles!)])
                        return
                    }
                })
            }
        }
    }

This method gets a userInfo Dictionary as input (there we can the WatchKit app asks from the iPhone app) and returns also a Dictionary. Here we have defined that we are asking for refreshData and we use a simple XML Parser (the code for the parser is available on Github alongside with the WatchKit demo used for this tutorial). After parsing the XML we need to archive the array as NSData so it can “travel” back to the Watch.

Now open the Interface.storyboard within the WatchKit app group in your project. Drag a table on your Interface Controller scene. Click the “Group” section in the Table Row Controller section and drag two Labels on the Table Row. Click the Group again and change the layout from horizontal to vertical and size to Size to Fit Content as shown in the screenshot below. Change the firs label font (the top one) to Heading.

Screen Shot 2015-03-15 at 20.18.03

Now add a new file in your WatchKit extension group and name it ArticleRow. Add the following code (yes, rows in WatchKit are just NSObjects)

class ArticleRow: NSObject {
    @IBOutlet weak var titleLabel: WKInterfaceLabel!
    @IBOutlet weak var detailLabel: WKInterfaceLabel!
}

Connect the IBOutlets to the row controller in the storyboard. Open  InterfaceController.swift file and drag also an Outlet Connection for the Table.

Add the following code in Interface Controller file:

var articles = [Article]()
override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        WKInterfaceController.openParentApplication(["request": "refreshData"], reply: { (replyInfo, error) -> Void in
            if let articleData = replyInfo["articleData"] as? NSData {
                if let articles = NSKeyedUnarchiver.unarchiveObjectWithData(articleData) as? [Article] {
                    self.articles = articles
                    self.reloadTable()
                }
            }
        })
    }

override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

func reloadTable() {
        newsTable.setNumberOfRows(articles.count, withRowType: "ArticleRow")
        
        for (index, article) in enumerate(articles) {
            if let row = newsTable.rowControllerAtIndex(index) as? ArticleRow {
                var currentTitle: String! = article.postTitle
                let finalTitleArray = currentTitle.componentsSeparatedByString("–")
                
                var finalTitle: String = finalTitleArray[1].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
                row.titleLabel.setText(finalTitle)
                row.detailLabel.setText(article.postDescription)
            }
        }
    }

In the awakeWithContext method, we call the parent app (our iOS app) for the data we need in order to fill the table. After getting the data we call reloadTable() (no there are no table view like delegate methods here). Note that we set text to Watchkit labels using the setText method and not dot syntax like in iOS apps. Go back to the storyboard, select the Interface Controller scene and set the class to ArticleRow in the identity inspector of the Table Row Controller and the identifier as ArticleRow in the attributes inspector.

Finally we need a detail view for our articles (the article class for the parsed objects is also included in the full tutorial code on Github).  Drag a new interface controller on the Interface.storyboard in the WatchKit group and add a push segue from the ArticleRow controller to the new controller.

Storyboard Watchkit

Drag a table on the new interface controller, add label on the row and create two new files in the WatchKit extension folder: one named ArticleDetailInterfaceController and called ArticleDetailRow. Set the same settings for the group layout and size for this controller. For the label set the number of lines to 0. Set the table row controller class and the identifier to “ArticleDetailsRow”. Add the following code to each of the newly created files.

import WatchKit
import Foundation

class ArticleDetailInterfaceController: WKInterfaceController {
    var article: Article!
    
    @IBOutlet weak var articleDetailTable: WKInterfaceTable!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        if let article = context as? Article {
            self.article = article
            self.reloadTable()
        }
    }
    
    //MARK: Table loading
    func reloadTable() {
        articleDetailTable.setNumberOfRows(1, withRowType: "ArticleDetailsRow")
        
        if let row = articleDetailTable.rowControllerAtIndex(0) as? ArticleDetailRow {
            var articleDescription: String! = article.postDescription
            row.textLabel.setText(articleDescription)
        }
    }
}

 

import WatchKit

class ArticleDetailRow: NSObject {
    @IBOutlet weak var textLabel: WKInterfaceLabel!
}

Connect the IBOutlets for the table and the row to the storyboard. After all the connections are in place select the segue and add “articleDetails” as identifier in the Attributes Inspector tab. Open InterfaceController.swift file and paste the following code in order to trigger the segue when a cell is tapped.

override func contextForSegueWithIdentifier(segueIdentifier: String,
        inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
            if segueIdentifier == "articleDetails" {
                let article = articles[rowIndex]
                return article
            }
            
            return nil
    }

Click and run the app. In order to see the WatchKit app, select the WatchKit app from the schemes and when the iOS Simulator launches, click Window>Displays and select one of the available WatchKit displays. The full code for this project can be found on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.