iOS 17 Widget Extension Development Practices and Solutions
The Kuwo Music team shares practical iOS 17 widget‑extension lessons, detailing container‑background adaptation, borderless button styling, AppIntent communication and hidden shortcuts, Link‑based navigation, lyric animation, refresh‑rate throttling, and splitting WidgetBundles to bypass the ten‑widget limit, all with ready‑to‑copy SwiftUI code.
This article, contributed by the Kuwo Music team of TME (Tencent Music Entertainment), records the practical experience of developing iOS 17 widget extensions. It is not a tutorial copied from WWDC, but a collection of real‑world problems encountered during deep development of widget features.
Background : With iOS 17 becoming widespread, many apps need to extend functionality to the home‑screen widget area. Kuwo Music, as part of the TME ecosystem, explores the new widget capabilities introduced in iOS 17.
Problems Encountered
iOS 17 container view adaptation issues.
SwiftUI Button not working correctly inside a widget.
AppIntent‑based button communication between the widget extension and the host app.
Making custom intents invisible in the Shortcuts app.
widgetURL and Link navigation conflicts.
Lyric animation implementation.
Refresh rate limits.
WidgetBundle exceeding the 10‑widget limit.
Deciding when to launch the host app (“拉端”) from a widget.
Pre‑development Glossary
The main app is referred to as host app .
The widget itself is called widget (extension target).
Creating a new target under the same bundle is the standard way to add a widget.
Development uses SwiftUI; Objective‑C compatibility is not covered.
Opening the host app from a widget is called 拉端 (launch‑app).
Deep links are used for URL‑based navigation.
Key Technical Solutions
1. Container Background Adaptation (iOS 17)
Use the new API to set the container background:
KWTestLargeWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)Wrap the code with availability checks at the bundle entry point to avoid affecting iOS 16 and earlier:
import WidgetKit
import SwiftUI
@available(iOSApplicationExtension 16.0, *)
@main
struct WidgetLauncher {
static func main() {
if #available(iOSApplicationExtension 17.0, *) {
WidgetsBundle17.main()
} else {
WidgetsBundle16.main()
}
}
}
struct WidgetsBundle16: WidgetBundle { var body: some Widget { KWxxxxWidget1() /* … */ } }
@available(iOSApplicationExtension 17.0, *)
struct WidgetsBundle17: WidgetBundle { var body: some Widget { KWxxxxWidget1() /* … */ } }Disable the default safe‑area margins when needed:
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: KWAppWidgetTestConfigurationIntent.self, provider: Provider()) { entry in
KWTestLargeWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.contentMarginsDisabled()
.configurationDisplayName("复古磁带机")
.description("让音乐以经典形态呈现")
}2. Button Styling in Widgets
Standard Button adds unwanted padding. Apply a borderless style:
Button(action: {
// action
}, label: {
Image("123")
.frame(minWidth: 90, minHeight: 90)
.aspectRatio(contentMode: .fill)
.tint(.clear)
.padding(0)
})
.buttonStyle(BorderlessButtonStyle())Only AppIntent ‑backed buttons or Toggle are supported in widgets.
3. AppIntent Communication
Define an intent that performs the desired work in the host app:
struct KWAppWidgetConfigurationLikeIntent: AudioPlaybackIntent {
static var title: LocalizedStringResource = "收藏或取消收藏当前歌曲"
static var description = IntentDescription("酷我音乐")
var info: KWWidgetItemInfo
init(info: KWWidgetItemInfo = .defaultInfo()) { self.info = info }
func perform() async throws -> some IntentResult {
try await KWWidgetAppIntentHandle.handleWidgetAppIntentFavorite()
return .result()
}
}The same intent can be duplicated in both the host app target and the widget extension target; the system extracts static metadata, allowing the widget to invoke host‑app logic without sharing runtime code.
4. Hiding Intents from Shortcuts
struct KWAppWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "酷我音乐"
static var description = IntentDescription("酷我音乐小组件.")
static var openAppWhenRun: Bool = true
static var isDiscoverable: Bool = false // hide from Shortcuts
func perform() async throws -> some IntentResult { .result() }
}5. Navigation with Link Instead of Multiple widgetURL
Only one widgetURL is allowed; using Link for each interactive view resolves the undefined behavior:
HStack(alignment: .bottom) {
Link(destination: URL(string: "sunyazhou://collectOrNot")!) {
Image(itemInfo!.didCollected ? "kw_widget_absorption_color_like" : "kw_widget_absorption_color_unlike")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight: itemSize.height, maxHeight: .infinity)
.border(.red)
}
Link(destination: URL(string: "sunyazhou://playOrPause")!) { /* … */ }
Link(destination: URL(string: "sunyazhou://playNext")!) { /* … */ }
}6. Lyric Animation
Text(entry.info.currLine)
.font(.system(size: 20).bold())
.foregroundColor(.white)
.lineLimit(2)
.truncationMode(.tail)
.id(entry.info.currLine)
.transition(.push(from: .bottom))
.animation(.easeInOut(duration: 0.6), value: entry.info.currLine)7. Refresh Rate Limiting
Widgets cannot be refreshed arbitrarily. Use a delayed reload to stay within system limits:
@objc class func reloadAllSystemWidgets() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(delayReloadAllSystemWidgets), object: nil)
self.perform(#selector(delayReloadAllSystemWidgets), with: nil, afterDelay: 0.2)
}
@objc private class func delayReloadAllSystemWidgets() {
WidgetCenter.shared.reloadTimelines(ofKind: "com.sunyazhou.widget1")
// … other kinds
}8. Exceeding the 10‑Widget Limit
Split widgets into multiple WidgetBundle groups and combine them with WidgetBundleBuilder.buildBlock (max 10 per bundle):
@available(iOS 17.0, *)
struct WidgetsBundleA: WidgetBundle {
var body: some Widget {
KWPlayMusicWidget1()
KWPlayMusicWidget2()
// … up to 10
}
}
@main
struct KWWidgetBundle: WidgetBundle {
var body: some Widget {
if #available(iOS 17.0, *) {
WidgetBundleBuilder.buildBlock(WidgetsBundleA().body, WidgetsBundleB().body)
}
}
}Conclusion
The article lists the most common pitfalls when building iOS 17 widgets—container adaptation, button handling, intent visibility, navigation, animation, refresh throttling, and bundle limits—and provides concrete Swift/SwiftUI code snippets that can be directly copied into a project.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.