iOS Swift 4: Today Extension

Today extension in iOS

There are various extensions that provide quick access to information without necessity to open app fully, in order to make iOS app convenient for use. All you need is just to swipe the main screen left and you will see a list of widgets — this is the Today extension which we will discuss in this article.

Today extension in iOS can only exist if you have main application.

Information from the database of the main application and images that were previously uploaded to the file system can be displayed here. Also here information via the network can be uploaded. Moreover, Today extension has an excellent possibility of rapid transition into the main application.

How To Add a Today Extension

Today extension, as was mentioned above, can be added to an existing project, so if you don’t have any, you should make a mobile app first.

So, when you have a project, it’s time to add the Today extension. For this, go to the menu File > New > Target:

After that, find the Today extension in the list of targets and click Next:

Then you will be prompted to enter your data’s name, select the project and the container-app to which your Today Extension will be assigned.

After clicking the Finish button you will need to activate the target schema for its further build and run.

A folder with the Today Extension name appears in our project’s files list. By the default, there is TodayViewController which implements the NCWidgetProviding protocol, MainInterface.storyboard, in which you may realize your widget’s UI and the Info.plist settings file.

Now we canrun our mobile application — go to the widgets panel, by swiping to the left on the home screen, and view our widget. If it hasn’t appeared yet, then click on «Edit» button and add it.

Today Extension Altitude

The widget’s functionality allows you to expand it and view more detailed information. To do this you should, for example, in viewDidLoad() make widgetLargestAvailableDisplayMode equal expanded:

    override func viewDidLoad() {
        super.viewDidLoad()
        self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded
    }

After this in the widget’s upper right corner will appear More/Less button:

But it will not work until you implement the NCWidgetProviding protocol method:

func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        if activeDisplayMode == .compact {
            self.preferredContentSize = maxSize
        } else if activeDisplayMode == .expanded {
            self.preferredContentSize = CGSize(width: maxSize.width, height: 150)
        }
    }

This method will be called each time you click on the More/Less button. At the moment activeDisplayMode can be equal to compact or expanded:

  • compact WidgetDisplayMode — the folded mode (the More button is shown). The Today Extension altitude is 110 (without the possibility to modify it), considering the system font is set by default. If it will be changed then the widget’s altitude will vary respectively;
  • expanded WidgetDisplayMode — the deployed mode (the Less button is shown). The Today Extension altitude can range from 110 to 440. Also, there is a possibility that when changing the system font the widget’s altitude may vary.

Depending on activeDisplayMode we set the necessary value to the self.preferredContentSize. For example, it can be the table high multiplied by a number of columns — for the table (to display all data at once) but don’t forget about the maxSize limit.

Today Extension Interface

To create the interface we may use a storyboard or add items in code into TodayViewController, all in the same way as when we created UIViewController for the main application.

For widget to always look actual, iOS captures snapshots of the last widget’s state before it leaves the screen. When the widget becomes visible again, the snapshot is displayed first, and only then actual information appears. To make widget update its state before snapshot, NCWidgetProviding protocol is used.

When the widget is invoked widgetPerformUpdateWithCompletionhandler, it needs to refresh its window and after this call the completionHandler block with the argument equal to one of the following constants:

  • NCUpdateResultNewData — new content requires window updating;
  • NCUpdateResultNoData — widget does not need updating;
  • NCUpdateResultFailed — an error has occurred while updating.

For example, let’s add two UIlabel’s using storyboard and make an animation for one of them.

Writing IBOutlets in TodayViewController:

@IBOutlet weak var textLabel:UILabel!
@IBOutlet weak var dateLabel:UILabel!

And connect them in the storyboard with a UIlabel that were already added using constraints:

Next, let’s make the animation method textLabel based on the current activeDisplayMode:

    func animateTextLabels() {
        let isExpandedMode = self.extensionContext?.widgetActiveDisplayMode == .expanded
        let scaleText:CGFloat = isExpandedMode ? 3 : 0.3
        UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
            self.textLabel.transform = .init(scaleX: scaleText, y: scaleText)
            self.dateLabel.transform = isExpandedMode ? .init(translationX: 0, y: 20) : .identity
        }) { (finished) in
            UIView.animate(withDuration: 0.3, animations: {
                self.textLabel.transform = .identity
            })
        }
    }

And will call it when clicking More/Less buttons:

    func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        animateTextLabels()
        if activeDisplayMode == .compact {
            self.preferredContentSize = maxSize
        } else if activeDisplayMode == .expanded {
            self.preferredContentSize = CGSize(width: maxSize.width, height: 150)
        }
    }

Then we will run the project, move on to the panel where widgets are displayed and look what happens. This is the simplest example of showing possibilities of work with widget’s UI elements.

UserDefaults in Today Extension

If your data is static or has been taken from the Foundation (e.g. Date) everything is simple. But if you need to output the data, written from the host application, then you need to create a container (group) for targets.

Add a group with your name and activate it. The group will also appear at the widget’s target, you need to activate it.

After the container is created, you can record the data, for example, using UserDefault and the group’s name:

let sharedDefaults = UserDefaults(suiteName: "group.sharingForTodayExtension")
sharedDefaults?.setValue("Stfalcon.com today extension tutorial", forKey: "customKey")

To show this data in the widget you need just call UserDefaults with the same suiteName:

let sharedDefaults = UserDefaults.init(suiteName: "group.sharingForTodayExtension")
 let text = sharedDefaults?.value(forKey: "customKey")

CoreData In Today Extension

To use CoreData, the work through the container is also required. But in this case, work with the container will be run with a database file. To make NSPersistentContainer read and to write data into the main container, you need to create its inheritor and override some methods:

 class PersistentContainer: NSPersistentContainer{
    override class func defaultDirectoryURL() -> URL{
        return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.sharingForTodayExtension")!
    }
 
    override init(name: String, managedObjectModel model: NSManagedObjectModel) {
        super.init(name: name, managedObjectModel: model)
    }
}

You also need to mark the target for this widget to have the access to the CoreData model.

You can now use the new PersistentContainer and work with it in the mobile app as well as in the widget.

Open The Mobile App Using Today Extension

To do this you need to open an URL with a scheme of your main container-app:

   let url = URL(string: "mainAppUrl://")!
        self.extensionContext?.open(url, completionHandler: { (success) in
            if (!success) {
                print("error: failed to open app from Today Extension")
            }
        })

In the URL you can specify, for example, the ID or object’s name, by using which (if to consider logic realization) a container-programme «learns» what ViewController, what request to send to the server, etc.

If the container-app still does not have its URL scheme, you can add it:

Stfalcon.com has more than 8 years of experience in implementation of complex solutions for medium and large businesses. Would like to order a mobile application, TMS or other complex web-based solution? Share your idea on info@stfalcon.com. We will create a unique project for you!