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:
- Declarative syntax is easier than dealing with old school constraints
- Less code for equivalent user interfaces
- Live Preview to iterate faster over the development life cycle
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 extendsUIViewController
- iMessage Extension
MSMessagesAppViewController
that extendsUIViewController
- Notification Content Extension
- Any
UIViewController
implementingUNNotificationContentExtension
- Share Extension
SLComposeServiceViewController
that extendsUIViewController
And any other extension…
Padlok
Still looking for the codes?
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
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
Still looking for the codes?
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.