In the code bundle for this chapter, you'll find a project called The Daily Quote. It's a straightforward app that displays a different inspirational quote to the user every day. The quotes are hardcoded in the Quotes.swift file and the current quote is stored in the UserDefaults store. The UI contains just two labels to show the current quote and the person that's quoted.
Even though the project is quite simple, each file contains something interesting. 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 Title 1 and the font for the quoted person is Caption 1. This is different from the default system font that you usually use. When you configure labels with these predefined styles, the text in the label will dynamically adjust based on the user's preferences. Adopting dynamic type such as this is requires only minimal effort and is great from an accessibility standpoint.
If you look at the QuoteViewController class, there are only a couple of lines involved in displaying a quote. The reason this code is so simple and concise is that a great deal of preparatory work was done in the Quote model file. Go ahead and open that file to see what's going on.
The Quotes struct is set up in such a way that it can be used as a simple database. The struct contains several static properties and methods to provide quotes from a predetermined list of quotes.
If you were to build a similar app and put it in the App Store, you'd probably want to download the quotes from a server and store it in Core Data, because pushing an update every time you want to add or remove a couple of quotes is a lot of effort for a simple change. There are only a couple of constant instance properties present on the Quote struct:
let text: String let creator: String
These properties are the ones that QuoteViewController reads and displays to the user.
Furthermore, UserDefaults is used to store which quote should be shown for the current day. UserDefaults is a simple data store that stores app-related settings. It's essentially a lightweight persistence layer that you can use to store simple objects. The Daily Quote makes use of UserDefaults to store the date on which the current quote was set as well as the index in the 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 whether a quote has 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 quoteIndex is retrieved from UserDefaults and returned as the current quote.
If you need a quick refresher on how to use UserDefaults in your app, go back to Chapter 15, Syncing Data with CloudKit.
Now that you are familiar with the contents of the starter project, add a new target to your project just like you have done before, but this time, select the Today Extension:
To see the default widget that Xcode generates for you, go ahead and run your extension. The first time you do this, your widget might not show up immediately. Don't worry if this is the case, build and run again, and your app should show up as a widget in the Today View.
Now let's see what kind of files and boilerplate code Xcode has generated for you. In the folder that Xcode created for your extension, you'll find a view controller, a storyboard and an Info.plist file. This structure is very similar to the structure that was used for the Notification Content Extension you created in the previous chapter.
To give the labels in the extension a little bit of room to breathe, 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 free-form and you can set a custom width and height. Leave the width as it is for now and set the height to 110. This size should be the smallest size at which our widget displays, and it gives you plenty of room to create the 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 adjust its width, so it covers the available space. After doing this, add the following constraints to the label:
- Leading space to Safe Area
- Top space to Safe Area
- Trailing space to Safe Area
Finally, set the number of lines for the label to 3 so the quote doesn't take up more than 3 lines. Now, drag out another label and position it right below the first label. Make sure this label has the same spacing from the superview's left edge and that it has the same with as the other label. Set its font style to Caption 1. Also, add the following constraints to this view:
- Leading space to Safe Area
- Trailing space to Safe Area
- Vertical spacing between this label and the label above it
Your layout in Interface Builder should resemble the following screenshot:
Go ahead and rerun your extension. Your final result should look similar to the one shown in the following screenshot:
Now that you have set up the layout for your extension, let's some outlets to the widget view controller so you can display the quote of the day inside of the widget:
@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 build this widget is to load and display the quote of the day in the widget. Since extensions do not communicate with their host apps directly, you will have to use App Groups again to implement a shared UserDefaults store. Sharing UserDefaults through App Groups will allow both your widget and app to retrieve the current quote index from UserDefaults, and ensures that both interfaces always show the same quote.
Create a new App Group for The Daily Quote and make sure the app and extension both belong to the same App Group. Also, make sure that you add Quote.swift to the extension target as well as the app target.
After enabling App Groups and making sure the app and the extension share the Quote struct, add the following code to TodayViewController.swift:
override func viewDidLoad() { super.viewDidLoad() updateWidget() } func updateWidget() { let quote = Quote.current quoteLabel.text = quote.text quoteCreator.text = quote.creator }
If you run the app now, you should see the current quote for the day shown in your widget, as illustrated in the following screenshot:
The widget currently loads the quote when its view controller is loaded. There are no guarantees about how and when your widget will be recreated. This means that you can't rely on viewDidLoad() to make sure that your widget always contains the most recent information. In the example that Xcode generated when you first created your extension, a method called widgetPerformUpdate(completionHandler:) can be found. This method is called regularly by the system to make sure that your widget can update its contents when needed.
The system passes a completionHandler to the update method that you must call to let iOS know whether your widget has loaded new data. This allows iOS to optimize the frequency and timing of calls to widgetPerformUpdate(completionHandler:) so your widget isn't doing more work than needed, and to make sure it updates when it's most likely to have new data. Add the following implementation for this widgetPerformUpdate(completionHandler:) to TodayViewController.swift:
func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) { let currentText = quoteLabel.text updateWidget() let newText = quoteLabel.text if currentText == newText { completionHandler(NCUpdateResult.noData) } else { completionHandler(NCUpdateResult.newData) } }
The preceding method updates the widget by calling updateWidget(), just like viewDidLoad does. Before doing this, the current text for the quote is stored. After updating the widget, the new text for the quote is stored. Based on the comparison of these two strings, the callback is informed about the result of the update request.
Some widgets in the Today View have a Show More button that can be tapped to expand the widget. You can implement this button in your own widget by setting the widgetLargestAvailableDisplayMode property on the notification context to the .expanded value. Setting the largest display mode to expanded tells the Today View that your widget needs the ability to show contents in a larger view. The following line of code demonstrates how to set the largest available display mode:
extensionContext?.widgetLargestAvailableDisplayMode = .expanded
When the user taps the Show More button that is rendered after setting the largest display mode, the widgetActiveDisplayModeDidChange(_:withMaximumSize:) method is called on your notification extension. In this method, you can set your preferredContentSize of TodayViewController to the size you would like your widget to be. This should always be smaller than the maximum size that is passed to the method because the Today View will not make your widget any bigger than the maximum size.
The following code is a short sample implementation for widgetActiveDisplayModeDidChange(_:withMaximumSize:) that determines the preferred content size based on the new display mode:
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { if activeDisplayMode == .compact { preferredContentSize = maxSize } else { preferredContentSize = CGSize(width: maxSize.width, height: 200) } }
With this information, you know everything you need to know to start implementing widgets for your own applications.