Widget: Today extension
There are several types of extensions Photo, Share, Custom Keyboard etc. Extensions are introduced in iOS 8 and are a very popular feature.
We will focus on Today extension also known as a widget and describe simple implementation of it.
First of all, Today and other extensions are simple “child” apps that are extending “parent” apps and cannot be distributed alone. Today extension can provide quick info to the user like a schedule, weather, most read news etc.
They must always have updated content, simple UI with low memory usage because users usually can have multiple extensions opened and the system will immediately terminate the extension which uses too much memory. Interactions are limited and because of that there is no keyboard access.
Enough theory, lets begin implementation. Open Xcode and in File menu select New > Project… (Swift as programming language). After project is created, open File menu and choose New > Target and in Application Extension section select Today Extension.
After that an alert will appear for creating new scheme for extension, just click Activate.
Xcode will create a new .storyboard file (MainInterface.storyboard by default), UIViewController class (TodayViewController by default) and one more Info.plist file. In Info.plist file you can change the extensions name under Bundle display name key and set a longer, more descriptive name.
Extension is just storyBoard and UIViewController which has all available methods (viewDidLoad(), viewDidAppear()…).
To set height value we can use preferredContentSize for height which we want. Max value for width will be width of screen. Add self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded to viewDidLoad() which creates More/Less button on extension view.
We must implement widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) protocol method which will be called when More/Less button is pressed. Size of extension can be set depending on state in that method.
if (activeDisplayMode == .compact) { self.preferredContentSize = maxSize } else { self.preferredContentSize = CGSize(width: maxSize.width, height: 150) } self.view.layoutIfNeeded()
Method widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) is part of NCWidgetProviding protocol, which we will mention later. Method widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets has been deprecated in iOS 10.
Before moving on, open MainInterface.storyboard. Add another UILabel and UIImageView to controller and connect them to TodayViewController class. Add fix constraints for height and width to UIImage and center with vertical constraint and add bottom, top, leading, trailing to title UILabel and subtitle UILabel.
After that we can add simple data fetching method to the extension of class. This will be used for fetching response data and UIImage data from url in response.
extension TodayViewController {
func fetchData(urlString: String, completion: @escaping (_ data: Data) -> ()) { guard let url = URL(string: urlString) else { return } let session = URLSession.shared session.dataTask(with: url) { (data, response, error) in if let data = data { completion(data) }else { print(error?.localizedDescription ?? "error with no description") } }.resume() } }
Set image url for fetching json data.
private let testUrlString = "https://api.myjson.com/bins/36vps"
Create struct for parsing and storing response data.
struct SimpleData { var title = "" var subTitle = "" var imageUrl = "" init(data:Data) { guard let jsonResponse = try? JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] else { return } title = jsonResponse["title"] as? String ?? "No title" subTitle = jsonResponse["subTitle"] as? String ?? "No subtitle" imageUrl = jsonResponse["imageUrl"] as? String ?? "" } }
Add simple method for updating UI with new data.
func updateUI(simpleData:SimpleData) { DispatchQueue.main.async { self.labelTitle.text = simpleData.title self.labelSubtitle.text = simpleData.subTitle self.view.layoutIfNeeded() } fetchData(urlString: simpleData.imageUrl) { (data) in DispatchQueue.main.async { self.imageView.image = UIImage(data: data) } } }
fetchData() will be set in viewDidLoad() and in widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)). It is used for updating view when extension is off screen. Update depends on constants (NCUpdateResult.newData, NCUpdateResult.noData, NCUpdateResult.failed) that will be return in completion handler.
For hiding empty content and unhiding when content has been received we can use also setHasContent(_ flag: Bool, forWidgetWithBundleIdentifier bundleID: String) where we pass bool that determines content state.
Now we can run the app and we should see a simple view similar to this one.
Open URL
Today extension is meant to be lightweight, for showing basic info and if the user wants something more interactive it would be good practice to transfer him to app.
For that kind of transfer, we usually use UIApplication.sharedApplication().openURL(url) but because the extension is not an app there is no UIApplication object available. Instead of that there is extension context that does the same thing extensionContext?.openURL(url, completionHandler: ((Bool) -> Void)?) .
We must set up url scheme in the application and call that url on the action in extension or some other app on device and application will open.
Add button in TodayViewController interface and connect IBAction to class. In action method call open url method extensionContext?.open(URL(string: “TestTodayExtension://”)!, completionHandler: nil). We can run our extension now and try button action to open app.
With url scheme we can open a specific screen in the app by handling it in method application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool in AppDelegate class.
Conclusion about widget
If you want to dive deeper, there is plenty of info about extensions in Apple Docs