Adding a Today Extension to your app

In this book's Git repository, you can find an app named The Daily Quote. If you want to follow along with this section of the chapter, it's a great idea to go ahead and check out the starter code for this app. If you look at Main.storyboard and the ViewController.swift file, you'll find that there isn't too much going on there. The interface just contains two labels, and the ViewController grabs a Quote and displays it.

Even though it doesn't look like much, each file contains something cool. If you select one of the labels in the Storyboard and examine its attributes, you'll find that the font for the quote itself is actually Title 1 and the font for the quote created is Caption 1. This is different from the default system font that we normally use. Selecting one of the predefined styles enables the text in our app to dynamically adjust to the user's preferences. This is great from an accessibility standpoint and it costs us only very little effort.

Tip

If your interface allows it, it's recommended that you make use of this technique, simply because it will make your app easier to use for all your users.

If you look at the ViewController class, there are only a couple of lines involved in displaying the quote. This code is simple and concise due to the great deal of preparation work that's been done in the Quote model file. Go ahead and open that file to see what's going on.

The Quotes struct is set up to act as its own factory. The struct contains several static properties and methods to generate quotes based on a predetermined list of quotes. If you were to build this app and put it in the App Store, you'd probably want to download these quotes from a server somehow, because pushing an update every time you want to add or remove a couple of quotes is quite a lot of effort for a simple change.

There are actually only regular properties present on the Quote struct:

let text: String 
let creator: String 

These properties are the ones that ViewController reads and displays to the user.

Furthermore, UserDefaults is used to store the quote for a given day. UserDefaults is a store that stores app-related settings. It's essentially a lightweight persistence layer that you can use to store simple objects. For The Daily Quote, UserDefaults is used to store the date on which the current quote was set as well as the index in our list of Quote instances that points to the current quote:

static var current: Quote { 
    if let lastQuoteDate = userDefaults.object(forKey: "lastQuoteDate") as? Date { 
        if NSCalendar.current.compare(Date(), to: lastQuoteDate, toGranularity: 
          .day) == .orderedDescending { 
            setNewQuote() 
        } 
    } else { 
        setNewQuote() 
    } 
     
    guard let quoteIndex = userDefaults.object(forKey: "quoteIndex") as? Int, 
        let quote = Quote.quote(atIndex: quoteIndex) 
        else { fatalError("Could not create a quote..") } 
     
    return quote 
} 
 
static func setNewQuote() { 
    let quoteIndex = Quote.randomIndex 
    let date = Date() 
     
    userDefaults.set(date, forKey: "lastQuoteDate") 
    userDefaults.set(quoteIndex, forKey: "quoteIndex") 
} 

The preceding snippet illustrates the process of retrieving and storing the current quote. First, the code checks if a quote has already been set before and, if so, it makes sure that the quote is at least a day old before generating a new one. Next, the current quote index is retrieved from the UserDefaults and returned as the current quote.

Setting and retrieving objects in UserDefaults is fairly straightforward. It's a simple key-value store that easily stores simple objects. We don't have to do anything special; we don't even have to manually save the UserDefaults like we would if this was CoreData. This makes UserDefaults a great candidate to store this type of non-sensitive, simple data.

Tip

Don't use UserDefaults to store privacy-sensitive data. It's not considered secure and the keychain should be used for this purpose. Also, make sure that you're not storing complex or repetitive data in UserDefaults. It's not a database, nor is it optimized for reading and writing a large number of times. Stick to simple, application-specific settings only.

The rest of the Quotes struct should speak for itself.

To add a Today extension to your app, open your project settings and make sure the sidebar button is enabled and you can see your Project and Targets:

Adding a Today Extension to your app

In the bottom-left corner of this sidebar, there's a plus button. If you click this, you can add additional targets to your app. You should see all the available iOS App Extensions; click on Today Extension and then click Next. Give your extension a name, for example The Daily Quote Widget, and click Finish.

Xcode will ask you if you want to activate a scheme with the name of your widget. Click Activate. This enables you to build and run your extension.

Next to the play and stop buttons in the top-right corner, you can now choose between the scheme for the app and the widget. Make sure to select the widget and then build and run your application:

Adding a Today Extension to your app

The first time you do this, your widget might not show up immediately. Don't worry if this is the case, just build and run again and your app should show up as a widget in the Today View.

Congratulations! You have just enabled your very first app extension. Let's see what kind of files and boilerplate code Xcode has added for us. In the Project navigator, you'll find a new group. This group is named after your widget and it contains an Info.plist file, a view controller, and a storyboard. This looks very similar to a fresh app project, except there is no assets folder and no launch screen.

If you open the storyboard in Interface Builder, you'll notice that there's a tiny view controller instead of one that looks like a device.

Note

You can actually change the display size for the view controller if you like, but note that the display size for the widget was taller than the visible size in the storyboard. This is due to the standard compact widget height iOS 10 uses. We can't influence this, but we know that we're constrained for space so it's good to make sure that your widget is flexible enough to look good on the compact size the Today View imposes on our widget.

To give our labels a little bit of breathing room, select the view controller (not the view itself) and click on the Size inspector. Here you'll see that the simulated size for the view controller is set to Freeform and you can set a custom width and height. Leave the width as is for now and set the height to 110. This size should be the smallest size at which our widget displays and it gives us plenty of room to lay out our interface. Also, delete the default label that has been added to the interface automatically.

Drag a UILabel into the view and set its font to Headline. Click the font icon in the Attributes inspector to change the font and select the Font dropdown to find the dynamic text styles. Position the label in the top-left corner using the blue helper lines and the add following constraints to the label:

  • Leading space to superview margin
  • Top space to vertical layout guide
  • Trailing space to superview margin

Finally, set the number of lines to three so the quote doesn't take up more than three lines. Now, drag out another label and position it right below the first label. Set its font style to Caption 1. Also, add the following constraints to this view:

  • Leading space to superview margin
  • Vertical spacing between this label and the label above it

Go ahead and run your extension again. Your layout should look as shown in the following screenshot:

Adding a Today Extension to your app

Okay, great! You have customized the layout for your first extension. Let's add some outlets to the widget view controller so we can display the quotes to our users:

@IBOutlet var quoteLabel: UILabel! 
@IBOutlet var quoteCreator: UILabel! 

Open the widget's storyboard file and connect the outlets as you've done before. Select the view controller, go to the Outlet Inspector and drag from the Outlet Inspector to the corresponding views to connect the outlets to the view.

The next step to building this widget is to display the quotes to our users. This is a problem, though, because the app itself is already grabbing and storing the quote for the day and we've already established that extensions do not communicate directly with their host apps.

For us, this means that we're stuck right now. We can't ask the app for a quote to display. We did store the quote index in UserDefaults, but unfortunately, the app and the extension don't share this store. We could settle for different quotes in each and just have the widget set and display its own quote. This isn't ideal, but if this is the best we can get we should live with it, right?

Update the following code in the TodayViewController file:

override func viewDidLoad() { 
    super.viewDidLoad() 
     
    updateWidget() 
} 
 
func updateWidget() { 
    let quote = Quote.current 
    quoteLabel.text = quote.text 
    quoteCreator.text = quote.creator 
} 

If you try to build and run your project now, you'll be presented with the following error:

Use of unresolved identifier `Quote` 

This is bad. Our extension and app don't share anything. They don't share code, they don't share data, and they can't communicate with each other. However, if you look at some of Apple's stock widgets, there seems to be some data sharing going on. And you can imagine that copying the Quote struct into your widget's code isn't maintainable for any serious application.

Luckily, there is a solution available. If you select the Quotes.swift file and look at the File inspector on the right-hand side of the window, you'll see a small header called Target Membership. Below this header, you can see your app target and your extension target. If you check the checkbox next to your extension target, your app is suddenly able to build and run. Awesome!

Adding a Today Extension to your app

To avoid confusion down the line, it's probably a good idea to create a new group at the root of the project, call it Shared and move Quotes.swift into this group. This should make it clear to you and any future developer that Quotes.swift is used in both targets.

Before we move on, there is just one more addition we need to make to the widget. Currently, we update the widget only when its view loads. The boilerplate code Xcode generated for us shows a method named widgetPerformUpdate(completionHandler:). We should use this method to update our widget, if needed, and then inform the callback about the result of the update action. Add the following implementation for this method:

func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) { 
    let currentText = quoteLabel.text 
    updateWidget() 
    let newText = quoteLabel.text 
     
    if currentText == newText { 
        completionHandler(NCUpdateResult.noData) 
    } else { 
        completionHandler(NCUpdateResult.newData) 
    } 
} 

This method updates the widget by calling updateWidget(), just like viewDidLoad does. Before doing this, we grab the current text for the quote. After doing that, we grab the new text for the quote. Based on the comparison of these two strings, we inform the callback about the result of the update request.

This wraps up our widget implementation for now. Let's explore the several ways users can discover and use your widget in iOS 10. So far, we've seen the Today View that Xcode automatically sends us to, but there are more ways for people to interact with your widget.

Discovering your widget in iOS 10

In iOS 10, there are several places where people can see and use your widget. Most places where your widget is displayed look similar. They're basically all the same Today View, except they have minor modifications. The first place where your widget is shown is on the lock screen. If a user swipes right on the lock screen, they are presented with their widgets. Secondly, a user can swipe downward from the top of the screen to see the notification center. Swiping right on this screen will present the same Today View that's presented on the lock screen. Finally, the Today View is available if the user swipes to the leftmost screen on their home screen.

There are quite a lot of places where users can discover and view your widget, but possibly the most interesting use case for your app is that users can view the widget directly on the home screen ever since iOS 10. All they need to do is force touch on your app's icon and the widget will pop right up:

Discovering your widget in iOS 10

Our project runs fine, and we can find our widget in several parts of iOS, but doesn't it feel a bit silly to show a different quote for each of our targets? Other apps seem to be able to synchronize data in the extension and app just fine and it's safe to assume that they don't use some kind of web API to retrieve information.

If you were expecting a solution to this problem by now, you are absolutely correct to do so. We can fix this. We can share data between our application and our extension using a feature called App Groups.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.68.28