Working with Widgets and App Clips
1. Creating WidgetKit Widgets
Example: Basic Widget Configuration
import WidgetKit
import SwiftUI
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent())
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
@main
struct SimpleWidget: Widget {
let kind: String = "SimpleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
Text("Hello, world!")
}
.configurationDisplayName("Simple Widget")
.description("This is an example widget.")
}
}
| Property | Value | Description |
|---|---|---|
| kind | String |
Unique identifier for the widget type |
| TimelineEntry | Hashable |
Represents a single entry in the widget's timeline |
| TimelineProvider | Protocol |
Defines how to generate timeline entries |
Note: Widgets must be declared in the app target and configured to support the widget extension.
Warning: Widget extensions must be built with Xcode 15 or later for iOS 17 support.
2. Defining Widget Timeline
| Property | Description | Values |
|---|---|---|
| TimelineEntry | Represents a single entry in the widget timeline | TimelineEntry |
| ReloadStrategy | Determines how often the timeline is reloaded | after(Date), never |
| EntryDate | Date and time for a timeline entry | Date |
Example: Creating a Timeline Entry
import WidgetKit
struct MyEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "MyWidget",
provider: Provider(),
placeholder: PlaceholderView(),
content: { entry in
MyWidgetEntryView(entry: entry)
}
)
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> MyEntry {
MyEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(in context: Context, completion: @escaping (MyEntry) -> ()) {
let entry = MyEntry(date: Date(), configuration: ConfigurationIntent())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MyEntry>) -> ()) {
let entry = MyEntry(date: Date(), configuration: ConfigurationIntent())
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 10, to: Date())
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate!))
completion(timeline)
}
}
Note: The
Timeline object is used to define how often the widget updates and what data it displays.
3. Using Timeline Providers
Timeline providers are responsible for generating the content that appears in widgets at specific times. They define when and what data to display.
| Property | Type | Description |
|---|---|---|
| initialTimeline | Timeline<Entry> |
The initial timeline to display when the widget is first added. |
| updateTimeline | Timeline<Entry> |
The updated timeline to display when the widget is refreshed. |
Example: Implementing a Timeline Provider
import WidgetKit
import SwiftUI
struct MyWidgetEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct MyTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> MyWidgetEntry {
MyWidgetEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> ()) {
let entry = MyWidgetEntry(date: Date(), configuration: ConfigurationIntent())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> ()) {
let entries: [MyWidgetEntry] = [
MyWidgetEntry(date: Date(), configuration: ConfigurationIntent()),
MyWidgetEntry(date: Date().addingTimeInterval(3600), configuration: ConfigurationIntent())
]
let timeline = Timeline(entries: entries, policy: .after(Date().addingTimeInterval(3600)))
completion(timeline)
}
}
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: MyTimelineProvider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
Note: The
getTimeline method is called by the system to determine what content to show in the widget at a given time. It's important to return entries that align with the widget's update policy.
Warning: Ensure that the timeline entries are generated efficiently to avoid performance issues. Avoid heavy computations or network requests in the
getTimeline method.
| Policy | Description |
|---|---|
| .after | Updates the timeline after a specified date. |
| .after | Updates the timeline after a specified date. |
| .after | Updates the timeline after a specified date. |
4. Configuring Widget Families
| Family | Description | Supported Sizes | Notes |
|---|---|---|---|
| systemSmall | Small widget size for compact displays | 1x1 |
Default family size for most widgets |
| systemMedium | Medium widget size for more content | 2x2 |
Best for displaying more data or controls |
| systemLarge | Large widget size for extensive content | 3x3 |
Useful for rich media or detailed views |
| systemExtraLarge | Extra large widget size for maximum space | 4x4 |
Not available on all device types |
| systemWide | Wide widget size for horizontal layouts | 4x1 |
Requires wide screen support (e.g., iPad) |
Example: Widget Family Configuration
import WidgetKit
import SwiftUI
struct MyWidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text("Widget Content")
}
}
}
@main
struct MyWidget: Widget {
let kind = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: Provider(),
family: .systemMedium
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
Note: Widget families must be declared in the
WidgetFamily enum and supported in the widget configuration.5. Implementing Widget Intents
| Property | Value | Description |
|---|---|---|
| Intent | WidgetIntent |
Defines the intent for a widget, including configuration and data binding. |
| IntentKey | String |
Unique identifier for an intent, used to associate data with a specific widget. |
| IntentValue | String |
Value associated with an intent key, used to configure widget behavior. |
Example: Defining a Widget Intent
import WidgetKit
import SwiftUI
struct MyWidgetEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> MyWidgetEntry {
MyWidgetEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> ()) {
let entry = MyWidgetEntry(date: Date(), configuration: ConfigurationIntent())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> ()) {
let entry = MyWidgetEntry(date: Date(), configuration: ConfigurationIntent())
let nextUpdate = Date().addingTimeInterval(60 * 60)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
struct ConfigurationIntent: WidgetIntent {
static let title = Text("Configuration")
static let description = Text("Configure the widget.")
@Parameter(title: "Name", description: "The name to display") var name: String
@Parameter(title: "Color", description: "The color to use") var color: Color
}
Note: Widget intents are defined using the
@Parameter attribute to specify configuration options that users can customize in the widget configuration screen.
Warning: Ensure that all widget intents are properly configured to avoid runtime errors when the widget is updated or reloaded.
6. Creating Interactive Widgets
| Property | Value | Description |
|---|---|---|
| Intent | .interactive |
Defines the widget as interactive, allowing user taps to trigger actions |
| Action | onTap |
Handler for when the widget is tapped, can navigate to App Clip or open URL |
Example: Basic Interactive Widget
import WidgetKit
import SwiftUI
struct InteractiveWidgetEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct InteractiveWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> InteractiveWidgetEntry {
InteractiveWidgetEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(in context: Context, completion: @escaping (InteractiveWidgetEntry) -> ()) {
let entry = InteractiveWidgetEntry(date: Date(), configuration: ConfigurationIntent())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<InteractiveWidgetEntry>) -> ()) {
let entry = InteractiveWidgetEntry(date: Date(), configuration: ConfigurationIntent())
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
@main
struct InteractiveWidget: Widget {
let kind: String = "InteractiveWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: InteractiveWidgetProvider()) { entry in
InteractiveWidgetEntryView(entry: entry)
}
.supportedFamilies([.systemSmall, .systemMedium])
.displayName("Interactive Widget")
.description("An interactive widget with tap actions.")
}
}
struct InteractiveWidgetEntryView: View {
var entry: InteractiveWidgetProvider.Entry
var body: some View {
VStack {
Text("Tap to interact")
.font(.headline)
Button("Open App Clip") {
// Handle tap action
}
}
}
}
Note: Interactive widgets require the
.interactive intent and must be configured to support tap actions using onTap.7. Building App Clips
Example: Basic App Clip Configuration
import AppClip
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.appClipInfo(
displayName: "MyApp Clip",
bundleIdentifier: "com.example.myapp.clip"
)
}
}
| Property | Value | Description |
|---|---|---|
| displayName | "MyApp Clip" |
Name shown to users when installing the App Clip |
| bundleIdentifier | "com.example.myapp.clip" |
Unique identifier for the App Clip |
Note: App Clips must be configured in the app's
Info.plist and have a corresponding AppClip target in Xcode.Warning: App Clips must not exceed 10MB in size to be eligible for installation.
Key Requirements
- App Clip must be built as a separate target in Xcode
- Bundle identifier must be unique and follow Apple's naming conventions
- App Clip content must not duplicate the full app's functionality
- App Clips must support deep links to the parent app
8. Configuring App Clip Experiences
| Property | Description | Value |
|---|---|---|
| appClip | Defines the App Clip configuration | true |
| appClip.url | URL to the App Clip | https://example.com/appclip |
| appClip.name | Name of the App Clip | MyAppClip |
| appClip.bundleID | Bundle identifier for the App Clip | com.example.myappclip |
Example: App Clip Configuration
struct MyAppClipApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowToolbarBackground(.automatic)
}
}
// App Clip configuration
@main
struct MyAppClip: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Note: App Clips must be configured in the
Info.plist with a valid URL and bundle identifier.
Warning: App Clips must be submitted to the App Store with a valid
AppClip entitlement.
9. Using App Clip Invocations
App Clip invocations allow users to launch an app clip directly from a physical location or digital content, such as a QR code or NFC tag.
| Property | Description | Values |
|---|---|---|
| url | The URL that triggers the App Clip | https://example.com/clip |
| type | Indicates the type of invocation | physical, digital |
| identifier | A unique identifier for the App Clip invocation | app-clip-invocation-123 |
Example: App Clip Invocation Setup
import AppClip
let invocation = AppClipInvocation(
url: "https://example.com/clip",
type: .digital,
identifier: "app-clip-invocation-123"
)
Note: App Clip invocations must be configured in the
Info.plist file to work correctly.
Warning: Ensure that the URL used for App Clip invocation is secure and uses HTTPS.
10. Converting App Clip Users to Full App
App Clips provide a lightweight way for users to interact with your app. Converting App Clip users to full app usage involves guiding them through a seamless transition that encourages installation and full app engagement.
| Method | Description | Notes |
|---|---|---|
| AppClip.url | URL that opens the App Clip | Used to provide context for conversion |
| AppClip.installation | Tracks whether the user installed the full app | Can be used to trigger conversion logic |
Example: Conversion Logic
import UIKit
import AppClip
class ConversionManager {
func convertAppClipUser() {
guard let appClipURL = URL(string: "https://example.com/app-clip") else { return }
UIApplication.shared.open(appClipURL) { success in
if success {
// User opened the App Clip, now guide them to full app
self.promptForFullAppInstallation()
}
}
}
func promptForFullAppInstallation() {
// Show a banner or alert to encourage full app installation
let alert = UIAlertController(title: "Install Full App", message: "Would you like to install the full app for more features?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Install", style: .default) { _ in
// Redirect to App Store or full app installation
})
alert.addAction(UIAlertAction(title: "Later", style: .cancel))
// Present the alert in your view controller
}
}
Note: Ensure that the App Clip provides a clear value proposition to encourage users to install the full app.
Warning: Avoid aggressive prompting that might lead to a poor user experience. Conversion should feel natural and beneficial.