Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

WatchKit Tutorial with Swift: Tables and Network Requests

$
0
0
Thank you for being a part of the Swift Spring Fling!

Continue your WatchKit adventures during the Swift Spring Fling!

Note from Ray: This is a bonus WatchKit tutorial released as part of the Spring Swift Fling. If you like what you see, be sure to check out WatchKit by Tutorials for even more on developing for the Apple Watch. We hope you enjoy! :]

You and I know that WatchKit apps are just iOS app extensions — but this isn’t always obvious from the user’s perspective. Users can launch the app from the watch and use it without being aware the code is actually running on their phone.

That means your extension can be started and stopped at a moment’s notice. If you have a long-running task such as finding the user’s location or fetching data from the network, you can’t rely on the extension to stay around long enough for those tasks to complete.

In our introductory WatchKit tutorial, you built a Bitcoin-tracking WatchKit app with a simple interface that performed the network request right in the extension.

In this tutorial, you’ll take things to the next level and build a WatchKit app that tracks a collection of cryptocurrencies, including Dogecoin and Litecoin. You’ll learn about tables, table rows, and use the watch-to-phone communication feature to trigger the network request and pass the data back to the Watch.

If you didn’t make enough on the Bitcoin exchange to save up for the Apple Watch Edition, it’s not too late to start mining some DOGE! :]

CoinTracker-doge1

Getting Started

Download the starter project and open it in Xcode. Remember to use Xcode 6.2 Beta 5! You’ll see an iOS app named CoinTracker with a table view interface listing the tracked currencies, like so:

CoinTracker-starter

The Xcode project includes a WatchKit app target and an empty interface controller so you can get started right away. Check out our introductory WatchKit tutorial if you want to learn how to set this up from scratch.

Open CoinsInterfaceController.swift inside the CoinTracker WatchKit Extension group. Add the following import underneath the other imports:

import CoinKit

The starter project includes the CoinKit framework, which contains the data structure and helper class you’ll be using throughout this tutorial.

Next, add the following properties to the CoinsInterfaceController class:

var coins = [Coin]()
let coinHelper = CoinHelper()

The framework will fetch the latest values of each coin and return Coin objects, which have the name of the currency and the exchange rate, among other values. You’ll store these in coins.

CoinHelper has the methods that perform the network requests. You won’t trigger this from the extension — remember, you’ll call out to the main iOS app for that! However, there are some useful methods here to cache currency values, as there could be a delay between the Watch app starting up and new data coming in. It’s good practice to have something to show the user on launch, even if it’s older cached values.

Now that the properties are in place, it’s time to set up the interface and display some data.

Creating WatchKit Tables

WatchKit tables are just as useful as their iOS counterpart, UITableView. They’re also much easier to use, since you don’t have to worry about data sources or delegates. Instead, support for tables is baked right into WKInterfaceController.

Still in CoinsInterfaceController.swift, add the following outlet to the class:

@IBOutlet weak var coinTable: WKInterfaceTable!

You always need a reference to your tables so you can set them up and add rows from code.

Open Interface.storyboard in the CoinTracker WatchKit App group, where you’ll see a single interface controller. Find Table in the object library and drag one to the interface like so:

CoinTracker-table1

As with table views in iOS, you can have many different kinds of rows. In this case, the table comes with one type of row to start and you’ll only need the one in this tutorial.

Notice how the table row has a group inside it – you’ll add all of the row contents to this group. Start by dragging two labels into the table row group as shown below:

CoinTracker-table2

Select the group (this is easiest to do from the Document Outline) and open the Attributes Inspector. Change Group\Layout to Vertical and Size\Height to Size to Fit Content. That will stack the two labels vertically; the containing group will grow to fit the labels automatically.

Select the top label, set its text to Coin name and change its font to the Headline style so it looks a little more prominent.

Select the bottom label and set its text to Price. At this point your interface should look like the following:

CoinTracker-table3

The interface looks good; it’s time to wire up all the outlets so you can add rows and set those labels to something meaningful.

Creating Table Connections

You already have an outlet ready for the table, but you need to connect those two labels in the table row to something.

In iOS, you might subclass UITableViewCell and add the outlets there. But the objects in WatchKit are much more lightweight than that. Remember, when you set labels or images from code, the phone will send only the modified data over the air to the watch. The complex bits are handled by WatchKit and the interface objects, so you don’t need some fancy object wrapping all of that inside your row.

Right-click (or control-click) on the CoinTracker WatchKit Extension group and click New File. Select the iOS\Source\Swift File template, name the new file CoinRow and click Create.

Open CoinRow.swift and add the following code to the file:

import WatchKit
 
class CoinRow: NSObject {
  @IBOutlet weak var titleLabel: WKInterfaceLabel!
  @IBOutlet weak var detailLabel: WKInterfaceLabel!
}

That’s right — your table row is simply a subclass of NSObject.

First, you need to import WatchKit to access the interface object definitions. Then you’ll declare two outlets for the labels.

Open Interface.storyboard. Control-drag from the interface controller icon to the table, then select coinTable from the Outlets popup to connect the outlet:

CoinTracker-outlet1

Next, select Table Row Controller in the Document Outline. Using the Identity Inspector, change the class to CoinRow. Then use the Attributes Inspector to set the Identifier to CoinRow as well.

Changing the class links the table row controller to the NSObject subclass you created. Later on, when you instruct the table to add some rows, the identifier will tell the table which type of row to create. Although you only need one type of row for this app, remember that your WatchKit App could potentially have many different types of rows.

Now you just need to connect the outlets to the two labels inside the row. Control-drag from CoinRow in the document library to Coin name, then select the titleLabel outlet like so:

CoinTracker-outlet2

Finally, Control-drag from CoinRow to the Price label, and select the detailLabel outlet.

That takes care of the interface and outlet connections! It’s time to get back to coding.

Displaying Table Data

First you’ll take care of the code that sets up the table rows.

Open CoinsInterfaceController.swift and add the following helper method to the class:

func reloadTable() {
  // 1
  coinTable.setNumberOfRows(10, withRowType: "CoinRow")
 
  for (index, coin) in enumerate(coins) {
    // 2
    if let row = coinTable.rowControllerAtIndex(index) as? CoinRow {
      // 3
      row.titleLabel.setText(coin.name)
      row.detailLabel.setText("\(coin.price)")
    }
  }
}

Here’s what’s going on in the above method:

  1. You set the number of rows and their type; the type string here is the identifier you set on the table row back in the storyboard. Since you don’t have any coin data yet, you’re hard-coding ten rows just to test things out.
  2. Once you have coin data, you enumerate through the array of coins. At this point the table has already created the ten row objects, so rowControllerAtIndex(_:) simply fetches the object for a particular row. To be safe, there’s some optional binding here to ensure your code is dealing with CoinRow objects.
  3. Finally, if the creation of your CoinRow succeeded, you set the text for the two labels.

Again, this approach is quite different from UITableView; you need to set up the table and all its rows in one shot; there’s no cellForRowAtIndexPath-like callback where you provide the rows on an as-needed basis.

Note: Since you need to populate all rows all at once, displaying a large data set can take a long time. Apple recommends limiting the number of rows to some reasonable number for your use case. For example, you might only display 20 rows in a news reader app with a “Show More” button at the bottom.

Next, find awakeWithContext(_:) and add the following code to the end of that method:

coins = coinHelper.cachedPrices()
reloadTable()

First, you get the set of cached coin prices. Since you haven’t made any network requests, this returns an empty array for now. Then you call reloadTable() to set up the table rows.

Ensure you’ve selected the CoinTracker WatchKit App scheme and build and run your project; you’ll see your table with ten test rows as shown below:

CoinTracker-run1

Excellent — you’ve set up the interface and some code to display a table the watch! Now you need some real data in there so you can start amassing your fortune! :]

Return to Xcode, stop the app, then modify the first line in reloadTable() that sets the number of rows like so:

if coinTable.numberOfRows != coins.count {
  coinTable.setNumberOfRows(coins.count, withRowType: "CoinRow")
}

That sets the number of rows to the actual number of coins in the array.

Note: There’s a bug in the current version of WatchKit where setting the number of rows to the same number already in a table causes the rows to act strangely. The workaround is what you see above – checking the existing number of rows before setting it.

Going From WatchKit to iOS

You’re probably used to iOS apps that wake up in the background to perform background refreshes or respond to push notifications. iOS 8.2 now gives you the ability to wake up the app via a call from the WatchKit extension!

Add the following code to the end of awakeWithContext(_:):

WKInterfaceController.openParentApplication(["request": "refreshData"],
 reply: { (replyInfo, error) -> Void in
  // TODO: process reply data
  NSLog("Reply: \(replyInfo)")
})

openParentApplication(_:reply:) is a class method on WKInterfaceController that forces the iOS app — that is, the parent application — to launch.

The first parameter is a user info dictionary that can contain any data you like, as long as it conforms to the plist data format. The iOS app will receive this object at the other end, so you can pass in any options you need here. In this case, there’s only one reason to call the parent application — to get the coin data — so technically you don’t need to put anything here, but the request key in the dictionary leaves the door open for future functionality beyond refreshData.

The second parameter is similar to a completion handler that’s called when the parent application finishes. WKInterfaceController will call the closure expression you pass in here, along with a dictionary that the parent can supply as a reply in replyInfo.

Going From iOS to WatchKit

You have the request ready to be sent to the iOS app, so the next step is to do the heavy lifting of the network request and send a reply back to the watch.

Open AppDelegate.swift in the CoinTracker group. This is the app delegate for the iOS app and contains all the entry-point code of the app.

Add the following method to the class:

func application(application: UIApplication!,
 handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!,
 reply: (([NSObject : AnyObject]!) -> Void)!) {
 
  // 1
  if let request = userInfo["request"] as? String {
    if request == "refreshData" {
      // 2
      let coinHelper = CoinHelper()
      let coins = coinHelper.requestPriceSynchronous()
 
      // 3
      reply(["coinData": NSKeyedArchiver.archivedDataWithRootObject(coins)])
      return
    }
  }
 
  // 4
  reply([:])
}

You’ll call this method in response to openParentApplication(_:reply:) on the WatchKit side. Here’s the play-by-play:

  1. You check for the request key and its value, just to be sure it’s really the watch app calling.
  2. Next, you instantiate CoinHelper to perform the network request. Note that you’re calling the synchronous version of the fetch request; if you performed a background fetch instead, the method would return right away and the reply would always be empty, which would be of no use to you.
  3. After the request comes back, you call the reply handler. coins is an array of Coin objects, so you need to archive the array into an NSData instance so it survives the trip back to WatchKit.
  4. If something goes wrong, the default action is to send back an empty dictionary.

Build and run the watch app and keep an eye on the console. The watch app itself won’t display any table rows, but you should see a hex dump of the NSData coming back from the iOS app in the console as shown in the following example:

CoinTracker WatchKit Extension[29705:1176700] Reply:
[coinData: <62706c69 73743030 d4010203 04050649 4a582476 65727369 6f6e5824
6f626a65 63747359 24617263 68697665 72542474 6f701200 0186a0ac 0708111b
1c232d2e 38394344 55246e75 6c6cd209 0a0b105a 4e532e6f 626a6563 74735624

That’s a good sign: it means the iOS app is indeed starting up and sending something back to the watch. If only it were something more useful than binary data! Are you up for the challenge of decoding the data and seeing what’s hiding in there?

001_ChallengeAccepted

Yeah, we thought so. Okay — the final stretch of code is coming up!

Collecting Your Coins

Open CoinsInterfaceController.swift and find awakeWithContext(_:). Replace the NSLog() call inside the openParentApplication closure expression with the following:

if let coinData = replyInfo["coinData"] as? NSData {
  if let coins = NSKeyedUnarchiver.unarchiveObjectWithData(coinData) as? [Coin] {
    self.coinHelper.cachePriceData(coins)
    self.coins = coins
    self.reloadTable()
  }
}

There are two levels of optional binding here: the first to ensure you received an NSData object in response, and the second to ensure what was archived inside the NSData is actually an array of Coin objects.

If everything checks out, you cache the data for later use. Then it’s just a matter of setting the coins property with the actual data and reloading the table rows.

Build and run your watch app; you should see some real data come through:

CoinTracker-run2

In a relatively small amount of code, you’ve sent a request from the watch to the phone, sent a request out to the network, received a response with data, then decoded the data and sent it from the phone back to the watch — that’s quite a trip! The glorious end result is that you can see four cryptocurrencies with a tap of the wrist so you can stay on top of your trading! :]

Adding Table Segues

There’s one final feature to look at: getting the table to do something when you tap on a row. The standard iOS action is to push a details screen onto the navigation stack. That’s exactly what you’ll do, but you’ll do it WatchKit-style instead.

You saw how easy it was to set up the table and rows for your WatchKit app, and navigation is just as easy. You don’t need to wrap anything in a navigation controller, since interface controllers are already set up for you to handle navigation stacks and table row taps.

Open Interface.storyboard and drag a new Interface Controller from the object library to the canvas area. Control-drag from your table row to the new interface controller to create a segue — make sure you drag from the empty part of the table row, not one of the labels!

CoinTracker-segue1

Select push in the Selection Segue popover to create a push segue:

CoinTracker-segue2

Select the segue and open the Attributes Inspector. Set the Identifier to CoinDetails. You’ll only have one segue in this app, but it’s a good idea to future-proof your app by naming your segues and checking for them by name in your code in case you add more segues later.

Passing Segue Data

If you’ve dealt with segues in iOS, you know things can get a little painful when passing data around: your prepareForSegue method tends to get quite long and there’s a lot of casting involved, among other details.

As always, WatchKit takes a cleaner approach. Every interface controller accepts a context parameter, which is an optional AnyObject. That means controllers have a built-in way to receive an initial bundle of data. As an extra bonus, WKInterfaceController also has a built-in way to send this context data right from a table segue. How handy is that?

Open CoinsInterfaceController.swift and add the following method to the class:

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

WatchKit calls this method when the user taps on a table row, as long as the row type has a segue connected to it. Your job is to return an object that will be passed into the destination controller as the context parameter. Here, you’re sending the Coin object for the selected row.

Receiving Context Data

Sending the data is only half the job, of course. You still need to process the incoming context and do something useful with it!

First, you’ll need a new class for the second interface controller that will show the coin details. Right-click (or control-click) on the CoinTracker WatchKit Extension group and click New File. Select the iOS\Source\Swift File template, name the new file CoinDetailInterfaceController, then click Create.

Open CoinDetailInterfaceController.swift and add the following code to the file:

import WatchKit
import CoinKit
 
class CoinDetailInterfaceController: WKInterfaceController {
  var coin: Coin!
 
  override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
 
    if let coin = context as? Coin {
      self.coin = coin
      setTitle(coin.name)
      NSLog("\(self.coin)")
    }
  }
}

First up are the usual imports for WatchKit and CoinKit; you need CoinKit so the Coin class is available to you.

The major point of entry for interface controllers is awakeFromContext(_:). Here, you start with optional binding to ensure the context passed in is a Coin object. If so, you keep a reference to the coin in a property. For now, all the visible interface work just sets the title and logs the coin details to the console.

Open Interface.storyboard and select the second interface controller that you added as a segue destination. In the Identity Inspector, set Class to CoinDetailInterfaceController to link the interface with the class you just created.

Build and run the watch app; tap on a row and you’ll see your (mostly) blank details controller appear:

CoinTracker-run3

Check out the console and you’ll see some details on the selected currency:

CoinTracker WatchKit Extension[30154:1201186] DOGE 0.00014345

Looks like the value of DOGE has gone up! :]

CoinTracker-doge2

Where to Go From Here?

You can download the final CoinTracker project here, complete with all the code and interface work from this tutorial.

You now have a WatchKit app that pulls fresh network data by asking the iOS app to do the fetching for you. You’ve also built a table to display the data, and learned the basics of tables and table rows in WatchKit.

You’ve also seen how easy it is to connect segues from Interface Builder. Although the segue works and you’ve even passed some context data across, the detail interface controller is mostly blank. You’d like it to show some details about the selected currency, wouldn’t you?

Fear not — in Part 2 of this tutorial, you’ll wrap up the interface for that second controller. You’ll also learn about glances, a special kind of interface controller that works a little like a Today extension that shows a short summary of information you can read quickly — at a glance, even! :]

Do you have any thoughts on tables or network downloads or anything else WatchKit-related? Share your thoughts in the forum discussion below!

WatchKit Tutorial with Swift: Tables and Network Requests is a post from: Ray Wenderlich

The post WatchKit Tutorial with Swift: Tables and Network Requests appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4384

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>