The WidgetBundle protocol inside WidgetKit allows you to configure supported widgets for your apps. Whenever you add one or more widgets, you’ll have to add a @main struct conforming to this protocol. Like SwiftUI views, you’ll return the supported widgets inside the body computed property.
You’ll not have issues with a consistent set of supported widgets. However, errors appear as soon as you want to return a set of widgets conditionally. Let’s dive into a workaround for this problem and help you support several widgets based on conditions.
Creating a WidgetBundle
Let’s start by looking at a standard implementation of a consistent set of widgets. For my Stock Analyzer app, this would look as follows:
@main
struct StockAnalyzerWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
StockAnalyzerWatchlistWidgets()
StockAnalyzerSymbolWidgets()
}
}
The WidgetBundle protocol requires us to return an opaque Widget
type through the computed body property. If you’re new to the some keyword, I encourage you to read my article Some keyword in Swift: Opaque types explained with code examples. In short, we’ll have to return a consistent type for the method’s scope.
The body method comes with the @WidgetBundleBuilder
attribute, which is comparable to the @ViewBuilder attribute in SwiftUI. It allows us to return a consistent set of widgets without using a return
keyword or array element.
Using conditions inside a WidgetBundle
The earlier shared example of a consistent set of widgets will work out great if your group of widgets doesn’t change inside the WidgetBundle. However, in the case of Stock Analyzer, I only wanted to add my StockAnalyzerSymbolWidgets
for iOS 16 and up. As soon as I added a control flow statement, I would run into the following error:
Xcode will tell you:
Closure containing control flow statement cannot be used with result builder ‘WidgetBundleBuilder’
As stated, WidgetBundleBuilder
is an example of a result builder. Read my article Result builders in Swift explained with code examples if you want to learn more about the implementation details. In this case, WidgetBundleBuilder
lacks support for control flow statements since it doesn’t implement the buildEither
result builder methods.
To work around this problem, we’ll have to create a separate method returning an opaque Widget type without the WidgetBundleBuilder
attribute. We will be able to use a control flow statement inside this method since we no longer have the restriction of the result builder:
@main
struct StockAnalyzerWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
widgets()
}
func widgets() -> some Widget {
if #available(iOS 16.0, *) {
return WidgetBundleBuilder.buildBlock(StockAnalyzerWatchlistWidgets(), StockAnalyzerSymbolWidgets())
} else {
return StockAnalyzerWatchlistWidgets()
}
}
}
We can make use of the WidgetBundleBuilder
static methods directly to get back an opaque Widget
return type in all cases. Based on the iOS 16 conditional if statement, we’re either returning both our widgets or just the single watchlist widget.
The body method will accept using the new widgets()
method since it’s returning a consistent opaque type for the method’s scope. Altogether, we’ve enabled ourselves to produce a different set of widgets conditionally inside our WidgetBundle.
Conclusion
WidgetBundle works excellently if you have a consistent set of widgets. Whenever you want to return a different set of widgets based on a condition, you’ll have to make use of a separate method returning an opaque type Widget
.
If you want to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!