Today Extension Tutorial: Getting Started

Michael Katz

Update Note:This tutorial has been updated to iOS 10 and Swift 3 by Michael Katz. The original tutorial was written by Chris Wagner.

iOS 8 introduced App Extensions: a way for you to share your app’s functionality with other apps or the OS itself.

One of these types of extensions is a Today Extension, also known as a Widget. These allow you to present information in the Notification Center and Lock Screen, and are a great way to provide immediate and up-to-date information that the user is interested in. Today Extensions can also appear on the Search screen, and on the quick action menu on the Home screen when using 3D Touch.

In this tutorial, you’ll write an interactive Today Extension that renders the current and recent market prices of a Bitcoin based on the United States Dollar.

Never has it been so easy to deliver valuable information so quickly to your users. Let’s get started!

Introducing Bitcoin

If you’re not familiar with Bitcoin, the short explanation is that it’s a digital cryptocurrency that’s still in its infancy. Aside from using it for peer-to-peer exchanges and purchases, Bitcoin trading allows the user to exchange it for a number of other cryptocurrencies like Dogecoin and Litecoin, and flat currency such as the US Dollar and the Euro.

As a relatively new currency, its market value fluctuates by the minute; there have been huge peaks and troughs in its short lifetime. Thus, it’s a perfect candidate for a Today Extension since investors will want up-to-the-second price quotes!

Introducing Crypticker

Since you’re writing a today extension, you’ll first need a host app to extend; meet Crypticker.

Crypticker is a simple app that displays the current Bitcoin price, the difference between yesterdays price and the current price, as well as a price history chart. The chart includes 30 days of history; tapping or swiping your finger on the chart reveals the exact price for a specific day in the past.

The extension will contain all of these features. Note that the swipe gesture often triggers sliding between the Today and Notifications sections within Notification Center, so it doesn’t really provide the best or most reliable user experience, but a single tap works quite well.

Getting Started

Download the Crypticker starter project to get started. The project contains the entire Crypticker app as described above, but please note that this tutorial will not focus on the development of the container app.

Build and run the project to see what you’re starting with:

Crypticker App

The app looks very similar to the screenshot above; the data displayed will, of course, depend on how the Bitcoin market looks right now. Touching the chart near the bottom of the view will draw a line and display the price for the relevant day.

BTC widget

For the unfamiliar, BTC is shorthand for Bitcoin; much like USD stands for United States Dollar. The Today Extension will render a scaled down version of Crypticker’s primary view.

Theoretically, the Crypticker app has the ability to show pricing for multiple Cryptocurrencies, but your today extension is specific to BTC. Therefore, its name shall be BTC Widget.

Note: Today Extensions, by nature, have just one simple purpose. If you wanted to provide information for another cryptocurrency, like Dogecoin, it would be best to package a second widget with the app or design your UI appropriately, perhaps like the Stocks widget.

By the end of the tutorial, your Today Extension will look something like this:

CryptickerExtension

Add a Today Extension target

Extensions are packaged as a separate binary from their host app. So you’ll need to add a Today Extension target to the Crypticker project.

In Xcode’s Project Navigator, select the Crypticker project and add a new target by selecting Editor\Add Target… When the template picker appears, choose iOS\ Application Extension, and then Today Extension. Click Next.

Add Today Extension Target

Set the Product Name to BTC Widget, and verify that the language is Swift, the project is Crypticker and Embed in Application is also Crypticker. Click Finish.

Name new target

When prompted, activate the BTC Widget scheme. As the text indicates, another Xcode scheme will be created for you.

Congratulations! BTC Widget will now appear in your list of targets.

add target

Make sure you select BTC Widget, then the General tab, and then press the + button under Linked Frameworks and Libraries.

Link Framework

Select CryptoCurrencyKit.framework and click Add.

CryptoCurrencyKit is a custom framework used by the Crypticker app to retrieve currency prices from the web and display them in a beautiful chart. Luckily for you, the incredibly kind and thoughtful developer behind Crypticker modularized the code into a framework, so that it can be shared between multiple targets. :]

In order to share code between a host app and its extensions you must use a custom framework. If you don’t, you’ll find yourself duplicating a lot of code and violating an important rule of software engineering: DRY – or, Don’t Repeat Yourself. I’ll say it again: “Don’t repeat yourself”.

This tutorial won’t go into much detail on frameworks themselves, as there’s enough information on them to fill their own tutorial. And wouldn’t you know we’ve done exactly that? ;] If you’d like to know more about creating and managing your own custom frameworks, check out this tutorial.

At this point, you’re ready to begin implementing the extension.

Notice there’s now a group in the Project navigator named after your new target, BTC Widget. This is where the Today Extension’s code is grouped, by default.

Expand the group and you’ll see there is a view controller, a storyboard file and an Info.plist file. Its target configuration also tells it to load its interface from MainInterface.storyboard, which contains a single view controller with the class set to TodayViewController.swift.

BTC Widget List of Files

You’ll notice some files you might expect to see are missing from the Today Extension template; like an app delegate for instance. Remember that today extensions run inside another host app, so they don’t go through the traditional app lifecycle.

In essence, the lifecycle of the today extension is mapped to the lifecycle of the TodayViewController. For example, TodayViewController‘s viewDidLoad method is called when the widget is launched, just like application(_:didFinishLaunchingWithOptions:) is called when the main app launches.

Open MainInterface.storyboard. You’ll see a clear view with a light Hello World label.

Make sure the BTC Widget scheme is selected in Xcode’s toolbar and build and run. This will launch the iOS Simulator and open the Notification Center, which in turn launches your widget. Notification Center is effectively the host app for Today Extensions. This also causes Xcode to attach its debugger to the widget’s process.

Default Widget

Behold your widget. Cool, right? Whilst this is super-exciting stuff, the widget clearly needs a little work. It’s time to make it do some interesting things!

Note: The name of the widget may be ‘CRYPTICKER’ – once you’ve run the host app the widget uses that name instead.

Build the Interface

Open MainInterface.storyboard and delete the label. Set the view to 110pts tall and 320pts wide in the Size Inspector. This is the default iPhone widget size.

Drag two Labels and a View from the Object Library onto the view controllers view.

  • Position one of the labels in the top left corner, and in the Attributes Inspector set its Text to $592.12 and its Color to Red: 33, Green: 73 and Blue: 108. Set the Font to System 36.0. This label will display the current market price. You want to make it nice and big so it’s easily legible in a quick glance.
  • Position the other label at the same height right of the one you’ve just set up, but against the right margin. In the Attributes Inspector set its Text to +1.23 and its Font to System 36.0. This displays the difference between yesterdays price and the current price.
  • Finally, position an empty view below the two labels, stretch it so it’s bottom and side edges are touching the containing view. In the Attributes Inspector set its Background to Clear Color, and in the Identity Inspector set its Class to JBLineChartView.

Note: There is a class named JBLineChartDotView that Xcode may suggest when typing, verify that you chose JBLineChartView.

The view and Document Outline should now look something like this:

Views placed

Don’t worry about laying things out exactly as shown, as you’ll soon be adding Auto Layout constraints to properly define the layout.

Now open TodayViewController.swift in the editor. Add this at the top of the file:

import CryptoCurrencyKit

This imports the CryptoCurrencyKit framework.

Next, update the class declaration, like this:

class TodayViewController: CurrencyDataViewController, NCWidgetProviding {

Making the TodayViewController a subclass of CurrencyDataViewController.

CurrencyDataViewController is included in CryptoCurrencyKit and is also used by the primary view within Crypticker. Since the widget and app will be displaying similar information through a UIViewController, it makes sense to put reusable components in a superclass and then sub-class that as requirements vary.

NCWidgetProviding is a protocol specific to widgets; there are two methods from the protocol that you’ll be implementing later on.

Since TodayViewController subclasses CurrencyDataViewController, it inherits outlets for the price label, price change label and line chart view. You now need to wire these up.

Open MainInterface.storyboard again.

In the Document Outline, ctrl+drag from Today View Controller to the price label (the one with its text set to $592.12). Select priceLabel from the popup to create the connection. Repeat for the other label, selecting priceChangeLabel from the popup. Finally, do the same for the Line Chart View, selecting lineChartView from the popup.

Wiring things up

Auto Layout

For your widget to be adaptive, you’ll need to set up Auto Layout constraints. The general idea is that views are designed with a single layout that can work on a variety of screen sizes. The view is considered adaptive when it can adapt to unknown future device metrics. This will be useful later when adding size expansion to the widget.

Select the Price Label label and then select Editor\Size to Fit Content. If the Size to Fit Content option is disabled in the menu, deselect the label, and then reselect it and try again; sometimes Xcode can be a little temperamental.

Next, using the Add New Constraints button at the bottom of the storyboard canvas, pin the Top and Leading space to 0 and 0 respectively. Make sure that Constrain to margins is turned on. Then click Add 2 Constraints.

Auto Layout 1

Select the Price Change Label label and again select Editor\Size to Fit Content. Then, using the Add New Constraints button, pin the Top and Trailing space both to 0.

Auto Layout 2

Finally, select the Line Chart View. Using the Add New Constraints button, pin its Leading and Trailing space to 0 and its Top and Bottom space to 8. Make sure that Constrain to margins is still turned on. Click Add 4 Constraints.

Auto Layout 3

From the Document Outline select the view containing the labels and Line Chart View, then choose Editor\Resolve Auto Layout Issues\All Views in Today View Controller\Update Frames. This will fix any Auto Layout warnings in the canvas by updating the frames of the views to match their constraints. If Update Frames is not enabled then you laid everything out perfect and it is unnecessary to run.

Auto Layout 4

Implementing TodayViewController.swift

Now the interface is in place and everything is wired up, open up TodayViewController.swift again.

You’ll notice you’re working with a bog-standard UIViewController subclass. Comforting, right? Although later you’ll encounter a new method called widgetPerformUpdate from the NCWidgetProviding protocol. You’ll learn more about that later.

This view controller is responsible for displaying the current price, price difference, and showing the price history in a line chart.

Now replace the boilerplate viewDidLoad method with the following implementation:

override func viewDidLoad() {
  super.viewDidLoad()
  lineChartView.delegate = self
  lineChartView.dataSource = self
  
  priceLabel.text = "--"
  priceChangeLabel.text = "--"
}

This method simply sets self as the data source and delegate for the line chart view, and sets some placeholder text on the two labels.

Now add the following method:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  
  fetchPrices { error in
    if error == nil {
      self.updatePriceLabel()
      self.updatePriceChangeLabel()
      self.updatePriceHistoryLineChart()
    }
  }
}

fetchPrices is defined in CurrencyDataViewController, and is an asynchronous call that takes a completion block. The method makes a request to the web-service mentioned at the beginning of the tutorial to obtain Bitcoin price information.

The method’s completion block updates both labels and the line chart. The update methods are defined for you in the super-class. They simply take the values retrieved by fetchPrices and format them appropriately for display.

Now it’s time to see what you have so far. Select the BTC Widget scheme. Build and run.

  • If Notification Center doesn’t appear, swipe down from the top of the screen to activate it.
  • If the widget doesn’t appear in Notification Center, you’ll need to add it via the Edit menu. Towards the bottom of the Today’s view content you will see an Edit button. Tap the button to reveal a menu of all Today Extensions that are installed on the system. Here you can enable, disable and re-order them as desired. Enable BTC Widget if it isn’t already.

BTC Widget Almost

Cool! Your widget now displays real-time Bitcoin pricing right in Notification Center. But you may have noticed a problem: the line chart looks pretty squished.

BTC-YUNO

Fortunately, Notification Center supports expandable widgets that can show more information.

At the bottom of viewDidLoad add the following:

extensionContext?.widgetLargestAvailableDisplayMode = .expanded

This tells the extension context that this widget supports an extended display. This will cause a “Show More” or “Show Less” button to automatically appear on the widget’s title bar.

Note: The main UIViewController of a today extension will have access to its extensionContext, which acts like UIApplication.shared, but for extensions. This provides functions for opening external URLs, and keys to listen for lifetime event notifications.

Next, add the following method:

func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
  let expanded = activeDisplayMode == .expanded
  preferredContentSize = expanded ? CGSize(width: maxSize.width, height: 200) : maxSize
}

widgetActiveDisplayModeDidChange is an optional NCWidgetProviding method. It is called in response to the user tapping the “Show More” or “Show Less” buttons. Setting the preferredContentSize will change the widget’s height, which in turn updates the chart’s height, giving it more room to breathe.maxSize is the maximum size allowed for the widget, given its display mode. For the .compact mode, the maximum size is also the minimum size, but for .expanded it could be much larger.

After updating the preferred size, you must reload the chart’s data so that it redraws based on the new layout.

You’ll do this in viewDidLayoutSubviews. Add the following to TodayViewController:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  updatePriceHistoryLineChart()
}

Make sure the BTC Widget scheme is selected. Build and run.

On the left, you’ll see how the widget appears when the widget is collapsed. On the right, you’ll see how it appears when expanded. Not too shabby!

Widget Expansion

Spruce up the UI

This looks OK, but it can still benefit from some visual tweaking. Since iOS places the widget on a blurred background, they are practically (and literally) begging for the ‘vibrancy’ effect.

Adding Vibrancy

Open MainInterface.storyboard again.

Drag a Visual Effect View with Blur from the object browser into the main view.

Drag the Line Chart View from the main view into the effect view’s subview. Click the Add New Constraints button and pin all four edges to the parent view with 0 padding. Make sure “Constrain to margins” is not selected. Then click Add 4 Constraints.

Update Line Chart View

Next, select the Visual Effect View and recreate the line chart’s previous constraints:

  1. Ctrl+drag from the effect view to the main view to bring up the constraint popup. Hold shift and select Leading Space to Container Margin, Trailing Space to Container Margin, and Vertical Spacing to Bottom Layout Guide. Then click Add Contraints.
    Effect View Constraints 1
  2. Ctrl+drag from the effect view to the Price Label and select Vertical Spacing.
    Effect View Constraints 2
  3. In the Size Inspector, change the Trailing and Leading Space constants to 0, and the Top Space and Bottom Space constants to 8.
    Effect View Constraints 3
  4. From the menu bar, choose Editor\Resolve Auto Layout Issues\All Views in Today View Controller\Update Frames.

Finally, in the Attributes Inspector check the Vibrancy box in the Visual Effect View section. This will cause the view to change from a dark color to a clear color.

Wire up the new view

Now open the Assistant Editor. Make sure TodayViewController.swift is the active file on the right.

Ctrl+drag from Visual Effects View in the storyboard editor to the top of the TodayViewController class. In the popup dialog make sure Connection is set to Outlet, Type is set to UIVisualEffectView, and enter vibrancyView for the Name. Click Connect.

Create vibrancyView Outlet

Then add the following line to the bottom of viewDidLoad:

vibrancyView.effect = UIVibrancyEffect.widgetPrimary()

This sets the vibrancy effect to the system-defined one for today extensions, ensuring that the coloring will be appropriate on screen.

Add the following to TodayViewController:

override func lineChartView(_ lineChartView: JBLineChartView!, colorForLineAtLineIndex lineIndex: UInt) -> UIColor! {
  return lineChartView.tintColor
}

The vibrancy effect sets the tintColor of anything in the contentView on a visual effect view. This is how labels and template images are automatically drawn with a vibrancy effect. For a custom view like JBLineChartView, the effect has to be applied manually. The lineChartView(_:colorForLineAtLineIndex:) delegate method is the place to do that here.

Build and run again.

Widget with Vibrancy

Very nice! Just a tweak to the line width and this could be downright beautiful.

At the top of TodayViewController add the following:

var lineWidth: CGFloat = 2.0

This variable will be used to control the line width.

Add this method:

private func toggleLineChart() {
  let expanded = extensionContext!.widgetActiveDisplayMode == .expanded
  if expanded {
    lineWidth = 4.0
  } else {
    lineWidth = 2.0
  }
}

This uses widgetActiveDisplayMode to determine if the widget is expanded or collapsed and sets the line width for the chart accordingly.

override func lineChartView(_ lineChartView: JBLineChartView!, widthForLineAtLineIndex lineIndex: UInt) -> CGFloat {
  return lineWidth
}

This delegate method returns lineWidth for the chart drawing routine’s use.

Finally, add the following to the bottom of widgetActiveDisplayModeDidChange:

toggleLineChart()

This calls your new method to propagate the line width.

Build and run again. This time, the line width will change along with the size change. How snazzy!

Widget With Line Width

To really see the vibrancy effect pop, set a colorful background. This can be done on the simulator by:

  1. Open the Photos app.
  2. Select an image.
  3. Tap the share icon.
  4. Select Use as Wallpaper from the bottom row.
  5. Tap Set and then Set Both.
  6. Build and run, again.

Vibrancy With Image

Make it Interactive

Widgets can be more than simple data displays, by supporting user interaction. The Crypticker app already supports tapping a position on the chart to display the price at that location. You can add that functionality to the widget when it’s expanded.

Go back to MainInterface.storyboard once again.

Drag a another Visual Effect View with Blur from the object browser into the main view.

In the Attributes Inspector check Vibrancy. This will cause the view to change from a dark color to a clear color.

In the Document Outline ctrl+drag from the new Visual Effect View to the previous Vibrancy View. Hold down Shift and select Top, Bottom, Leading, and Trailing. Click Add Constraints. This will place this new view in the same spot and size as the chart view.

Second Effect View

Next, drag a Label into the subview of the Visual Effect View. Pin this label to the top and center of its parent view by ctrl+dragging from the label into the parent view and selecting Top Space to Visual Effect View and Center Horizontally in Visual Effect View.

Second Label Constraints

Change the label’s text to be empty.

Select Editor\Resolve Auto Layout Issues\All Views in Today View Controller\Update Frames to rearrange the views. The label should now be invisible on the storyboard, but don’t worry… it’s still there :]

Aligned Label

In the Document Outline, ctrl+drag from the Today View Controller to the new label, and set its outlet to priceOnDayLabel.

Now the new label is almost wired up.

Open the Assistant Editor once again, and create an outlet for the new visual effects view in TodayViewController. Call it priceSelectionVibrancyView.

Connect Second Vibrancy View to Outlet

In viewDidLoad add this line to set the vibrancy effect:

priceSelectionVibrancyView.effect = UIVibrancyEffect.widgetSecondary()

The widgetSecondary vibrancy is a slightly different effect to be used for data that is ancillary to the main data. For this widget, the price at an earlier date on the graph certainly meets that criteria.

Note: Each UIVisualEffectView view can only have one type of vibrancy effect. Two different effects views are needed here to support both types of vibrancy.

Next, update toggleLineChart as follows:

private func toggleLineChart() {
  let expanded = extensionContext!.widgetActiveDisplayMode == .expanded
  if expanded {
    lineWidth = 4.0
    priceOnDayLabel.isHidden = false
  } else {
    lineWidth = 2.0
    priceOnDayLabel.isHidden = true
  }
  priceOnDayLabel.text = ""
}

In addition to changing the chart line width, this now hides or shows the label.

Now add these delegate methods:

func lineChartView(_ lineChartView: JBLineChartView!, didSelectLineAtIndex lineIndex: UInt, horizontalIndex: UInt) {
  if let prices = prices {
    let price = prices[Int(horizontalIndex)]
    updatePriceOnDayLabel(price)
  }
}

func didUnselectLineInLineChartView(_ lineChartView: JBLineChartView!) {
  priceOnDayLabel.text = ""
}

These simply update the label’s text when the user taps on the line chart.

Build and run. Expand the widget and tap on a point in the graph. You will see the price displayed, and at a slightly lighter color than the graph line.

Widget With Price On Day

Note: If you’re testing on the Simulator a quick ‘tap’ may not be enough to trigger displaying the label – so try a holding the mouse button down a little longer to make it appear.

Show Up On The Home Screen

By default, if there is only one widget in an application, it will show up automatically in the shortcut menu when using 3D Touch on the app’s icon on the home screen. The widget that shows up there can also be explicitly set if you want to choose which one will appear.

Open Info.plist under Supporting Files for Crypticker.

Use Editor\Add Item to add a new row. Choose Home Screen Widget from the drop down (or UIApplicationShortcutWidget if showing raw keys). In the Value column enter the widget’s Bundle Identifier. The Bundle Identifier for the widget can be found on the General tab of the target info pane.

Info plist item

Build and run the app. Press the Home button (Cmd+Shift+H in the Simulator), and then 3D Touch the app icon. The widget should now appear.

Home Screen Widget

Note: You may not be able to test this on the Simulator unless you have a Mac with a force-touch trackpad.

Wow. You get additional shortcut menu functionality for free! Even though only the collapsed size is available, you can’t beat the price.

Keep the Widget Up To Date

Your last order of business is to add support to your widget to update its view when it’s off-screen, by allowing the system to create a snapshot. The system does this periodically to help your widget stay up to date.

Replace the existing implementation of widgetPerformUpdate with the following code:

func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
  fetchPrices { error in
    if error == nil {
      self.updatePriceLabel()
      self.updatePriceChangeLabel()
      self.updatePriceHistoryLineChart()
      completionHandler(.newData)
    } else {
      completionHandler(.failed)
    }
  }
}

This method does the following:

  • Fetch the current price data from the web service by calling fetchPrices.
  • If there’s no error the interface is updated.
  • Finally – and as required by the NCWidgetProviding protocol – the function calls the system-provided completion block with the .newData enumeration.
  • In the event of an error, the completion block is called with the .failed enumeration. This informs the system that no new data is available and the existing snapshot should be used.

And that wraps up your Today Extension! You can download the final project here.

Where To Go From Here?

Download the final project here.

As an enterprising developer, you might want to take another look at your existing apps and think about how you can update them with Today Extensions. Take it a step further and dream up new app ideas that exploit the possibilities of Today Extensions.

If you’d like to learn more about creating other types of extensions, check out our iOS 8 App Extensions Tech Talk Video where you can learn about Photo Editing Extensions, Share Extensions, Action Extensions, and more!

We can’t wait to see what you come up with, and hope to have your Today Extensions at the top of our Notification Centers soon! We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Michael Katz

Michael Katz envisions a world where mobile apps always work, respect users’ privacy, and integrate well with their users’ life. When not coding, he can be found with his family playing board games, brewing, gardening, and watching the Yankees.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 44 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 17 total!

Podcast Team

... 8 total!

Recruitment Team

... 9 total!