Leveraging SwiftUI for any app extension

Since 2020, Widgets fully embrace SwiftUI by being a system extension fully based on them.

Building SwiftUI views instead of UIKit views is advantageous, including:

Because Padlok consisted on a lot of SwiftUI views, and also to leverage the advantages cited above, I wanted to implement a Notification Content Extension using mostly exclusively SwiftUI views. But this method also work for any system extension that present views, including:

Custom Keyboard Extension
UIInputViewController that extends UIViewController
iMessage Extension
MSMessagesAppViewController that extends UIViewController
Notification Content Extension
Any UIViewController implementing UNNotificationContentExtension
Share Extension
SLComposeServiceViewController that extends UIViewController

And any other extension…

Padlok notification content extension

Padlok Notification built in SwiftUI

Padlok app icon

Padlok

Still looking for the codes?

Learn more

Containing UIHostingController with constraints

As we cannot use UIHostingController directly because most of root controller are already some subclasses of UIViewController. Instead we are going to use a child view controller

/// Exemple of an iMessage app extension
final class MessagesViewController: MSMessagesAppViewController {
    private var contained: UIViewController? {
        willSet {
            guard let contained else {
                return
            }
            // When removing the contained view, call UIKit required lifecycle methods
            contained.willMove(toParent: nil)
            contained.view.removeFromSuperview()
            contained.removeFromParent()
        }
        didSet {
            guard let contained else {
                return
            }
            // To add contained view, call UIKit required lifecycle methods
            addChild(contained)
            view.addSubview(contained.view)
            contained.didMove(toParent: self)
            // We need to add some constraints to fill the view
            contained.view.translatesAutoresizingMaskIntoConstraints = false
            contained.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            contained.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
            contained.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
            contained.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        }
    }

    (...)
}

Clear background color

In some extensions like in Keyboard extensions, you might wanna keep the background clear so you get the neat gray used by default for keyboards.

But it turns out UIHostingController is adding a white background by default. Luckily it’s easy enough to remove:

// Create your Hosting Controller
let controller = UIHostingController(rootView: MyView())
// Remove the background
controller.view.backgroundColor = .clear
// Use it
contained = controller

Tint color

Somehow, I couldn’t auto-wire tint color in my extensions, but it’s easy enough to do by being able to access the Color asset:

override func viewDidLoad() {
    super.viewDidLoad()
    view.tintColor = (...)
}

Notification Content Extension: Self sizing issue

Notification Content extension expect us to size ourself using .preferredContentSize API.

It’s easy to get the minimal size of a SwiftUI view using UIKit APIs on the UIHostingController itself in the didSet method for the contained attribute:

let size = contained.view.sizeThatFits(view.bounds.size)
preferredContentSize = CGSize(width: size.width, height: size.height)

Specifically for Padlok, I found that adding a 8pt top padding worked well:


preferredContentSize = CGSize(width: size.width, height: size.height + 8)
contained.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 8).isActive = true

The case of live Previews

Almost every extensions will forbid you to preview directly in the extension target

Error: Previews are unsupported in Message Extension

This sadly have no solution, beside having your views included in your App Target as well, and using this target when previewing your views.

In Padlok, my shared views, and my extension views are in a Local Package that support Previews. The extension then consist only in the small bridge between UIKit and SwiftUI, and a little extension logic.

And the view is imported from this package, allowing me to leverage the whole Live Preview experience for my extensions.

Padlok app icon

Padlok

Still looking for the codes?

Learn more

Don’t miss a thing!

Don't miss any of my indie dev stories, app updates, or upcoming creations!
Stay in the loop and be the first to experience my apps, betas and stories of my indie journey.

Thank you for registering!
You’ll retrieve all of my latest news!

Your email is sadly invalid.
Can you try again?

An error occurred while registering.
Please try again