Creating Today Widgets

      2 Comments on Creating Today Widgets

Today widgets are small view controllers that your users can add to their 'Today' view. They are good for displaying the most relevant information to your users.Today widgets are small view controllers that your users can add to their ‘Today’ view. They are good for displaying the most relevant information to your users. In this article we’re going to create a today widget and display a latest article for the accompanying app for this blog. Let’s jump in…

Today Extension

Today extensions are simply targets that get embedded into your main app target. You can create more than one today extension. When you open your project, click on the little plus symbol in your targets list and select ‘Today Extension’ template:

Give a name to your extension, I suggest giving it a human-readable name, because it will be displayed in the list of extensions:

When you click ‘Finish’ Xcode will prompt you to activate the scheme for your new target, go ahead and activate it:

Now you can see your new target in the list of targets:

You can also see that Xcode created a new folder for you where you can keep the files added to your target:

Before moving on, select your target, open the ‘General’ tab and make sure that the version and build number match your main target, otherwise you won’t be able to submit the app:

That’s most of it for setting up the project. There’s one more thing to do…

App Group

Extensions are embedded into your bundle, but they run as a separate process with their own memory address space and their own container. So, naturally, the extension can’t access instances of classes of the main target. If you try and share data by saving it into the documents directory it won’t work either, because extensions have their own containers.

There’s an easy way to share data between your app and the extension. You can use ‘App Groups’ for that. Think of an App Group as a shared container that all your apps can access:

We have to enable app groups for our main target and the extension target. Select your main target and select the ‘Capabilities’ tab. Find ‘App Group’ and flick the switch to enable it:

When you enable it, click on the little plus symbol to create a new group, you will be prompted to enter the group identifier. For example, you can name your group identifier something like this:

When you’re finished with this step your group will display in the list of groups, and it should be selected by default, if it’s not, select it. Before doing this step, I would suggest that you log in to your developer account using Xcode, because Xcode will automagically create all of this for you. If you’re not logged in, you’ll have to do all of this manually on the developer portal.

You’ll have to enable ‘App Groups’ for your today extension target as well. The process is pretty much the same, except, you won’t have to create the group. The group should be displayed in the list as soon as you enable ‘App Groups’.

Now we can code πŸ™‚

Main Target Code

In our main app target we’ll have a simple class that will get the latest post from our data source and save it to the shared container. You’re probably familiar with UserDefaults. UserDefaults has a custom initialiser that will take in the group id of your shared container and it will essentially behave like a shared dictionary between your targets. This is exactly what we need, and we’ll be passing simple data around so it’s perfect. If you need to pass larger quantities of data, use the file manager instead.

We’ll share data between the targets using the UserDefaults, we’ll also share our constants file where we’ll keep all the keys. In this example all the keys will be in one file called ‘Key.swift’ you need to include this file in all the targets that will access it.

For example, your file might look something like this:

enum Key {
    static let URLScheme = "agostiniTech"
    static let GroupID = "group.agostini.tech.sharedData"
    static let PostID = "postID"
    static let PostTitle = "postTitle"
    static let PostDescription = "postDescription"
    static let PostImage = "postImage"
}

And make sure your target membership is selected:

All that’s left is to actually fetch the latest article and save it in the UserDefaults. This will be specific for your app, the decision when to get the data, and what to save will be entirely up to you. Here I’ll show you the class I’m using for this example.

The class is actually quite small, here’s the whole class:

protocol GetLatestPostUseCase {
    func getLatestPost(onCompleted: ((WPPostItem?) -> ())?)
}

class GetLatestPost: GetLatestPostUseCase
{
    private let getPosts: GetPostsUseCase
    private let getDetails: GetPostDetailsUseCase
    private let persistence: UserDefaults?
    
    init(getPosts: GetPostsUseCase = GetPostsFB(),
         getDetails: GetPostDetailsUseCase = GetPostDetails(),
         persistence: UserDefaults? = UserDefaults(suiteName: Key.GroupID)) {
        self.getPosts = getPosts
        self.getDetails = getDetails
        self.persistence = persistence
    }
    
    public func getLatestPost(onCompleted: ((WPPostItem?) -> ())?) {
        getPosts.getPosts { (items) in
            
            guard let item = items.first else {
                onCompleted?(nil)
                return
            }
            
            self.getDetails.getImage(forPost: item) { (remoteItem, image) in
                self.savePost(remoteItem, image)
                onCompleted?(remoteItem)
            }
        }
    }
    
    private func savePost(_ post: WPPostItem, _ image: UIImage?) {
        persistence?.set(post.postID, forKey: Key.PostID)
        persistence?.set(post.title, forKey: Key.PostTitle)
        persistence?.set(post.excerpt, forKey: Key.PostDescription)
        if let anImage = image {
            persistence?.set(UIImagePNGRepresentation(anImage), forKey: Key.PostImage)
        }
        persistence?.synchronize()
    }
}

We can see in the initialiser we’re instantiating UserDefaults with the shared container id. We have the function to get all the post items, and we fetch the first item from it. The important function in this class is the ‘savePost’ function. If you read through it, it should be pretty self-explanatory. The only comment I’ll add is, don’t save large amounts of data in the UserDefaults and don’t save images (like I did πŸ™‚ ). If you need to save large amounts of data, use the file manager. The only reason I’m saving this image here is because I wanted to keep this example simple and in my specific case this image is pretty small.

That’s it for the main target. Let’s move to the extension code.

Extension code

In the extension target, you’ll find your storyboard and your view controller. Go ahead and design your storyboard (use auto layout, of course πŸ™‚ ), mine looks something like this:

In the view controller we’ll be fetching the data directly from UserDefaults. If your app is a bit more complex, I suggest using a separate controller for this, or even, sharing your controllers between the targets like we did with the keys. Just, mind you, once you start sharing code between targets, all the classes that are being used by the shared class must also be shared, so you have to be careful when designing your controllers.

In the view controller we’ll have a variable to access our UserDefaults, like so:

private var persistence: UserDefaults? = UserDefaults(suiteName: Key.GroupID)

We’ll have a private function that we’ll call from viewDidAppear to update the UI elements:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
        
    updateUI()
}
    
private func updateUI() {
    itemID = persistence?.value(forKey: Key.PostID) as? Int
    titleLabel.text = persistence?.value(forKey: Key.PostTitle) as? String
    descriptionLabel.text = persistence?.value(forKey: Key.PostDescription) as? String
        
    if let imageData = persistence?.value(forKey: Key.PostImage) as? Data {
        let image = UIImage(data: imageData)
        imageView.image = image
    }
}

Nothing funny here, it’s all pretty standard. iOS system will occasionally call a method that will give your widget a chance to redraw itself, we might as well take advantage of that, we can update our UI from this method too:

func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
        
    let oldItemID = itemID
    updateUI()
        
    var result = NCUpdateResult.newData
    if oldItemID == itemID {
        result = .noData
    }
        
    completionHandler(result)
}

Now build and run your main target so your latest article would get saved in the UserDefaults. Then build and run the today widget (if you don’t see your widget appearing, run it again):

If everything went as planned, you should see something like this:

Force Touch

As an extra bonus, your widget will appear when you force touch on your app icon, how cool is that πŸ™‚

We’re almost done, just one little detail πŸ™‚

Responding to User Actions

At the moment, if you tap on the latest article in the widget, nothing happens. We want to actually open the latest article that the user selected in the main app, so let’s implement that quickly. We’ll be using a custom URL scheme to pass data back to the app. First thing you’ll need to do is register your custom URL scheme. Select your main target, and go to the ‘Info’ tab, at the bottom you’ll find ‘URL Types’, add your custom URL scheme there:

I created a custom class to format the URLs that will be shared between the extension and the main target, here it is:

public enum URLFormatter {
    
    private static let URLScheme = "agostiniTech"
    private static let OpenPost = "openPost"
    
    case openPost(postID: Int)
    
    var url: URL? {
        switch self {
        case .openPost(let postID):
            return URL(string: URLFormatter.URLScheme + "://\(URLFormatter.OpenPost)?\(Key.PostID)=\(postID)")
        }
    }
}

You might have a button somewhere in your extension like I do, when your user selects that button you would format the URL using this class and open the main app:

@IBAction func itemSelectedAction() {
    guard let id = itemID, let url = URLFormatter.openPost(postID: id).url else {
        return
    }
    extensionContext?.open(url)
}

One thing to note is that we’re using the extensionContext to open the URL, remember, your extensions have ho access to instances in the main target.

Last thing to do is to handle this URL in the app delegate. Implement the method in your AppDelegate:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
    if let postParamValue = queryItems?.filter({ $0.name == Key.PostID }).first?.value, let postID = Int(postParamValue) {
        showPost(withID: postID)
    }
    return true
}

In this function we’ll get the postID of the item, and we’ll call a function that will display this post to the user.

Conclusion

Today widgets are very simple view controllers that you can do in a few hours and they can be pretty useful to your users. I hope that in this not so short post you learned how to do them and that you’ll be enriching your apps with them.

As always, have a nice day πŸ™‚

Dejan.

More resources

2 thoughts on “Creating Today Widgets

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.