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
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.