How to make your first WatchKit app

NOTE: This tutorial assumes a familiarity with Swift. If you do not know any Swift at all, please learn the basics (google ‘Intro to Swift’).

You can view this project on Github

Hello everyone! For the first tutorial on this site I thought it’d only be appropriate to make a tutorial on making your first WatchKit app. The app will be a simple guessing Reddit app (this is now Nano for Reddit started).

First things first, make sure you have Xcode installed and setup, but if you’re reading this tutorial I assume you’ve already got that far.

At the start I’ll be using lots of images to help new comers, but as I progress I will be moving towards more text-based tutorials, so if you can’t keep up, just google any terms I use 🙂

Let’s get started

New Project

Open up Xcode.app and navigate to File > New > Project. Under “Choose a template for your new project:”, select watchOS, then iOS App with WatchKit App

Give your app a name, for the purpose of this tutorial we’ll use RedditTutorial. Ensure the language is set to Swift,

and uncheck all of the options at the bottom.

Choose a location to save it, and then it Create.

For the purpose of this tutorial we’re going to ignore the iOS app, so you can go ahead and close that folder.

Setting up our UI

Before we start connecting our app to Reddit, we’re going to setup our user interface. To begin, click on Interface.storyboard in the left panel, if you cannot see this panel, press ⌘0.

Your Interface.storyboard should look like this

Press ⇧⌘L or click the button shown in the screenshot below

Search for Table and drag it into the storyboard watch.

Now search for a Label, and drag it onto the newly created Table Row

Your storyboard should now look like this:

Now before we can hook this up to code, there is a little customising we need to do. First things first, select your label, and open the Attributes Inspector

Now we’re going to set the Lines property to 0.

Why? Well when specifying the number of lines, obviously 1 means 1 line, 2 means 2 lines, and so on. But 0 is different, 0 lines essentially means infinite lines, or in English, your label will resize itself based on the number of lines.

Now we need to create new class for our new Table Row, thankfully this is easy.

Go to File > New > File or press ⌘N. Create a new watchOS > WatchKit class.

Name it RedditCell

Under Subclass of, set that too NSObject.

Set the Group to RedditTutorial WatchKit App and the Targets to RedditTutorial WatchKit Extension and RedditTutorial

That file should open up, but just jump back to your Storyboard like you did at the beginning.

Now with your Storyboard open, open the Assistant Editor by clicking ⌥⌘↵ or by clicking the button shown below.

At the top of the Assistant Editor, click on Automatic and navigate to your RedditCell

In the Storyboard navigation panel, click on your Table Row Controller, and click on the Identity Inspector (it’s just to the left of the Attributes Inspector). Under Class, set that to RedditCell

Make sure you have “Inherit Module From Target” checked too!

Now holding Control (⌃), drag from your Label to your RedditCell file in the Assistant Editor, set its name to postTitle. What this does is create an IBOutlet which you can use to customise your label in code. IBOutlets (or Interface Builder Outlets are a way to connect your code to your user interface.

Now in your Assistant Editor using the same process you used to navigate to RedditCell.swift, navigate to your InterfaceController.swift file.

Now use the process you used before to connect your table to your InterfaceController.swift file, we’ll call it redditTable

Lets get coding!

Now our simplistic user interface is hooked up to our code and we can begin connecting to Reddit. We want the user to input what subreddit they’d like to visit, in order to do this we used the function

self.presentTextInputController(withSuggestions: String?, allowedInputMode: WKTextInputMode, completion: [Any]? -> Void)

But first let us create our own function, we’ll call it getUserInput:

func getUserInput(gotInput: @escaping (_: String) -> Void){
        
}

There’s a lot to break down here. Assuming you’re familiar with func, let’s look at gotInput: @escaping (_: String) -> Void, this gives the function an argument, which is actually a function itself (given by the -> Void), these are called callbacks, and you can read more about them here. What our function will do is get input from the user, and then call the gotInput function with the users input. That looks like this:

func getUserInput(gotInput: @escaping (_: String) -> Void){
self.presentTextInputController(withSuggestions: ["AskReddit"], allowedInputMode: .plain, completion: {result in
guard let userInput = result?.first as? String else {return}
gotInput(userInput)
})
}

This function presents a text input to the user, and calls the gotInput function upon completion.

Now in your awake(withContext context: Any?) function, add the follow lines:

self.getUserInput(gotInput: {input in
   print(input)
})

Now that we’ve got the users input, lets print(input) and run our app to confirm it’s working.

In the top left of Xcode, click RedditTutorial and select an RedditTutorial WatchKit App and select an Apple Watch to run your app on. If no devices show up, google “How to setup Apple Watch with simulator Xcode”

Now press ⌘R to run your application. An Apple Watch simulator should open, and it will shortly prompt the user for input, upon completion the inputted text will print to the console

Now it’s time to treat that user input like a subreddit name, and fetch the subreddit.

In order to make network requests in Swift, you use a URLSession, so let’s give it a shot.

In the closure of your self.getUserInput function, replacing print(input) with the following:

First we must create our URL, thankfully Reddit lets us append .json to the end of just about any URL on the site to get a json representation of it. So the URL will look like http://reddit.com/r/(input).json,

if let redditURL = URL(string: "https://reddit.com/r/(input).json") {
                
}

Now we can create our URLRequest object:

var request = URLRequest(url: redditURL)

Now we need to set a custom User-Agent for our request, in order to differentiate a request from your client to someone else’ss on Reddit (and to avoid rate limiting from default headers).

request.setValue("Replace this with something long and unique, about this long works well", forHTTPHeaderField: "User-Agent")

Now we can actually make our request!

Your full awake(withContext content: Any?) function should now look like this

override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        // Configure interface objects here.
        self.getUserInput(gotInput: {input in
            if let redditURL = URL(string: "https://reddit.com/r/(input).json") {
                var request = URLRequest(url: redditURL)
                request.setValue("Replace this with something long and unique, about this long works well", forHTTPHeaderField: "User-Agent")
                
                URLSession.shared.dataTask(with: request, completionHandler: {data, response, error in
                    
                }).resume()
            }
        })
    }

But now we have to do something with that data field, and for that we have to parse the JSON response from Reddit, this is where things get tricky. To do this we are going to use the Codable protocol introduced in Swift 4. Now if you don’t follow along, that’s ok, just copy the code I write and you should be golden, but I’d encourage you to do some of your own research into Codable.

First things first, let’s look at the JSON provided to us from Reddit, you can view a live version of this here,

Uck! What is what!?

Let’s use jsonprettyprint.com to make some sense of it

That looks much better!

We can see that data holds an object called children, which holds an array of each post. Each post then has two objects, kind and data. We’re going to look at the data child. This object holds all the information you would want about a post, as well as the "title".

Create a new file called RedditPosts.swift

Now create two structs, RedditPosts, and RedditPost, and make them both inherit the Codable protocol.

struct RedditPosts: Codable{

}
struct RedditPost: Codable{

}

RedditPost is going to be our individual post, and RedditPosts will hold an array of each post.

Add var posts: [RedditPost?] to RedditPosts.

We need to add an initialiser to RedditPosts, we do that with

init?(json: [String: Any]) {

}

Inside that initialiser, we need to access the children object from the JSON. First we need to get the top data object, since json is a [String: Any] object, we can do that with

guard let data = json["data"] as? [String: Any] else {return nil}

, this takes data and takes the contents. Now we need to access children, which is done is much the same way

guard let children = data["children"] as? [[String: Any]] else {return nil}

Now we have a children object of type [[String: Any]], meaning it holds an array of [String: Any] objects.

Now let’s move to the other struct, RedditPost. Create a title variable of type String

var title: String

We need to make an initialiser for this also,

init?(json: [String: Any]) {
        guard let data = json["data"] as? [String: Any], let title = data["title"] as? String else {return nil}
        self.title = title
    }

What the above does is takes the inputted json object, and grab the title. Think of this like children[index][data][title]. We then assign this title to the title variable.

Now go back to your RedditPost struct and add this to your initialiser.

self.posts = children.map {RedditPost(json: $0)}''

Your struct should now look like this:

struct RedditPosts: Codable{
    var posts: [RedditPost?]
    
    init?(json: [String: Any]) {
        guard let data = json["data"] as? [String: Any], let children = data["children"] as? [[String: Any]] else {return nil}
        self.posts = children.map {RedditPost(json: $0)}
    }
}

Now jump back to your InterfaceController.swift, go to the closure of your URLSession call, now we need to safely unwrap our data object.

That’s easy: guard let data = data else {return}

Now we to open a do block, as JSON parsing can throw error

do{
        //This is where we’ll do our JSON parsing
} catch {
        print(error)
}

Inside of the do{} block, we need to create our jsonObject, for this we use JSONSerialization.jsonObject()

do{
        if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]{
//Now we can create our RedditPosts object
        }
                            
  }
                        
} catch {
  print(error)
}

Now we’ve created our jsonObject which is a [String: Any], which if you may remember is the type we use to initialise a RedditPosts object, so we can do just that.

guard let posts = RedditPosts(json: jsonObject) else {return}

Now we have a RedditPosts object, which contains a posts object, which contains a bunch of RedditPost objects, still with me?

So now all of the posts are stored at posts.posts. Let’s add them to our table! Jump back to your Interface.storyboard and select your Reddit Cell, in the Attributes Inspector, give your cell an identifier, we’ll call it RedditPostCell

Now in your InterfaceController.swift, let’s create a new function called setupTable, which takes an argument of an array of RedditPost.

func setupTable(withPosts posts: [RedditPost?]){
        self.redditTable.setNumberOfRows(posts.count, withRowType: "RedditPostCell")
}

In here we can set the number of rows to show on our table, it should be equal to the number of posts Reddit returned to us.

self.redditTable.setNumberOfRows(posts.count, withRowType: "RedditPostCell")

Now lets iterate over each post,

for (index, element) in posts.enumerated(){
            
}

We need to take the index of each post and lookup the rowController at that index, and assign the row the title of each post.

for (index, element) in posts.enumerated(){
            guard let row = self.redditTable.rowController(at: index) as? RedditCell else {continue}
            row.postTitle.setText(element?.title)
}

Go back to your URLSession closure and call this new setupTable function the posts from your posts object:

guard let posts = RedditPosts(json: jsonObject) else {return
self.setupTable(withPosts: posts.posts)

Run your code, and voila, you can read the top posts from any subreddit (provided you enter it correctly)

But you may notice a small bug, the post titles are being cut off, didn’t we already address this by setting the line count to 0? Now we have a different problem, every row is the same height, but this is an easy fix.

Go to your Interface.storyboard, and select your row controller (labeled RedditPostCell, and select the Group inside it.

Open the Attributes Inspector, and change the Height from default to ‘Size to Fit Content’

Now run your app again, and voila, it works!

Now while this app may be simple, you’ve learned how to use WKInterfaceTable, creating custom cells, networking, JSON parsing, and user input.

You can view this project on Github

Challenge

I think I want to end each post with a challenge. This posts challenge is to make it so you can view the posts text, useful for subreddit like tifu.

Hint: Just like we got the title of a post, we can get the selftext object too 😉

If you solve this challenge, tweet it to me @WillRBishop!

Thanks!

Thanks for reading the first tutorial. I know it’s not perfect, so comment below what you’d like to see revised for the next tutorial.

One Comment

Darko January 6, 2019

you have an error when creating request url where you put /(input).json instead of /\(input).json . Also, all the codable dance could have been replaced by just pasting the json to qt app that creates codable model from JSON. Also, you could have pasted the model, there is no need to write about codable in watch app tutorial. But all in all ok tutorial, keep going.

Comments closed