English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский
WidgetDevelopmentKit is a Swift package for developers who build widgets for their iOS apps. It contains ready-made SwiftUI views for Home Screen, Lock Screen and StandBy widgets, chart views sized for widgets, helpers for timelines and for the data the app shares with its widget, App Intents for widget buttons, and ActivityKit attributes and views for Live Activities. You still write the widget extension; the package saves you the views and the date, storage and retry logic that every widget needs.
- 20 Home Screen templates — weather, stats, finance, calendar, countdown, clock, notes, quote, music, podcast, news, fitness rings, habit tracker, to-do, water, battery and more.
- Lock Screen and StandBy — 15 accessory views (circular, rectangular, inline) and 7 StandBy views.
- Timeline helpers — entry dates, reload policies and a snapshot store with expiry, so the widget reads what the app saved through an App Group.
- Interactive widgets — 5 App Intents and 8 views with buttons that run them.
- Live Activities — 10
ActivityAttributestypes with Lock Screen and compact Dynamic Island views, and a state machine that rejects out-of-order updates. - Tested logic — the date, number and retry code is covered by Swift Testing tests,
including edge cases such as
NaN, zero goals and daylight-saving days.
1. Add the package
Your app needs a widget extension. If it has none yet, choose File › New › Target… in Xcode and pick Widget Extension. Then choose File › Add Package Dependencies…, paste this URL into the search field at the top right and choose Add Package:
https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git
In the next dialog, set Add to Target for WidgetDevelopmentKit to your widget
extension and choose Add Package.
2. Import the module
At the top of a Swift file in the widget extension:
import WidgetDevelopmentKit3. Use it
Add this widget to the extension. It counts down to a date with the package's
CountdownWidget view. Then list LaunchCountdownWidget() in the extension's
WidgetBundle, the type marked @main.
struct LaunchEntry: TimelineEntry {
let date: Date
}
struct LaunchProvider: TimelineProvider {
func placeholder(in context: Context) -> LaunchEntry {
LaunchEntry(date: .now)
}
func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) {
completion(LaunchEntry(date: .now))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<LaunchEntry>) -> Void) {
// One entry per hour for the next six hours, then WidgetKit asks again.
let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init)
completion(Timeline(entries: entries, policy: .atEnd))
}
}
struct LaunchCountdownWidget: Widget {
let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now
var body: some WidgetConfiguration {
StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in
CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date)
}
.configurationDisplayName("Countdown")
.supportedFamilies([.systemSmall])
}
}Run the app on an iPhone simulator. On the Home Screen, touch and hold an empty area, tap
Edit › Add Widget, find your app and add Countdown. The same code is in
Examples/WidgetKitExample.
In Xcode, choose File › Add Package Dependencies… and enter the repository URL:
https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git
Or add it to the dependencies in your Package.swift:
dependencies: [
.package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0")
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit")
])
]Releases start at 2.0.0; the earlier v1.0.0 tag predates the current code and does not
build. The package has no third-party dependencies.
Every Swift example in this section is taken from
Examples/WidgetKitExample, which CI builds, or from
ReadmeExamplesTests.swift,
which is compiled with the tests. A test fails if a block here no longer matches that code.
TimelineSchedule produces the entry dates, TimelineBuilder sorts the entries and turns a
WidgetUpdatePolicy into WidgetKit's reload policy, and WidgetSnapshotStore reads what the
app saved to the shared App Group.
struct StepsProvider: TimelineProvider {
let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard)
func placeholder(in context: Context) -> StepsEntry {
StepsEntry(date: .now, steps: 6_000)
}
func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) {
completion(placeholder(in: context))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<StepsEntry>) -> Void) {
completion(makeTimeline(now: .now))
}
func makeTimeline(now: Date) -> Timeline<StepsEntry> {
// Written by the app; stale data is better than an empty widget.
let steps = store.staleValue(Int.self, forKey: "steps") ?? 0
let entries = TimelineSchedule
.dates(from: now, every: 15 * 60, count: 4)
.map { StepsEntry(date: $0, steps: steps) }
return TimelineBuilder(entries: entries)
.reloading(.every(minutes: 60), from: now)
.build()
}
}The app writes the value with a lifetime and asks WidgetKit to reload the widget:
try store.save(steps, forKey: "steps", lifetime: 30 * 60)
WidgetCenter.shared.reloadTimelines(ofKind: "steps")Add the App Groups capability to the app and the extension with the same identifier.
The store uses UserDefaults, which is not encrypted, so keep secrets out of it.
struct StepsWidgetView: View {
let entry: StepsEntry
var body: some View {
WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint])
.padding()
.containerBackground(.fill.tertiary, for: .widget)
.widgetURL(WidgetDeepLink(scheme: "steps", host: "today"))
}
}Views that show time-dependent values (CountdownWidget, StandByCountdownWidget,
LockScreenCountdownWidget) take a referenceDate; pass entry.date so each entry renders
its own moment.
struct WaterButton: View {
var body: some View {
Button(intent: IncrementIntent(counterId: "water", amount: 250)) {
Label("+250 ml", systemImage: "drop.fill")
}
}
}The intents run in the widget extension and write to WidgetStateStore.shared, which uses
the App Group named by the WidgetDevelopmentKitAppGroup key in Info.plist. Add the key to
both the app and the extension, and register the package's intents:
struct AppIntentsRoot: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[WidgetDevelopmentKitIntents.self]
}
}struct RandomQuote: Decodable {
let text: String
}
func loadQuote() async throws -> RandomQuote {
guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) }
let endpoint = Endpoint(base: base)
.appendingPath("v1/quotes/random")
.adding(query: "lang", "en")
guard let request = endpoint.request else { throw URLError(.badURL) }
return try await WidgetDataLoader().load(RandomQuote.self, from: request)
}WidgetDataLoader uses an ephemeral session with short timeouts that does not wait for
connectivity. It retries 5xx and 408 responses, timeouts and offline errors (once by
default; set a RetryPolicy to change it), but not 4xx responses or decoding errors.
The images are rendered from the package's views with ImageRenderer by
scripts/render-screenshots.sh. They are drawn outside
WidgetKit, so the widget frame, Lock Screen vibrancy and StandBy night mode are approximated
rather than produced by the system.
To try the widgets yourself, open
Examples/WidgetKitExample/WidgetKitExample.xcodeproj and run
the WidgetKitExample scheme on an iPhone simulator; Examples/README.md
explains the three widgets and how to add them to the Home Screen.
Reference documentation and articles:
- API reference on GitHub Pages, built from the DocC comments in the source.
- In Xcode: Product › Build Documentation, or
xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'.
- Getting Started: add a widget extension and the package, share data through an App Group, build the timeline and render an entry.
- Interactive Widgets: configure the App Group, register the intents, add buttons and read their state in the provider and the app.
| Platform | Minimum version |
|---|---|
| iOS | 17.0 |
| macOS | 14.0 |
| Toolchain | Swift 6.0 |
Lock Screen views and Live Activities are iOS only. The macOS target exists so the
platform-independent code and its tests run with swift test. CI builds with the default
Xcode of GitHub's macos-26 image; local development uses Xcode 27. Older Xcode versions have
not been tried.
Sources/WidgetDevelopmentKit/
├── Core/ Pure helpers: clamping, formatting, month grid, countdown math
├── Timeline/ Update policy, timeline builder and schedule, snapshot store
├── Networking/ Data loader with retry policy, endpoint builder
├── Configuration/ Deep links, hex colors, App Intents entities
├── Interactive/ App Intents, shared state store, interactive views
├── Templates/ Home Screen views
├── Charts/ Swift Charts views
├── LockScreen/ Accessory family views (iOS)
├── StandBy/ StandBy views
├── LiveActivities/ ActivityKit attributes and views (iOS), state machine
├── UI/ Timeline scrubber
└── WidgetDevelopmentKit.docc/
Views stay thin: arithmetic that can go wrong with real data (zero goals, NaN, reversed
date ranges, daylight-saving days) lives in Core and Timeline as pure functions with unit
tests. Types that touch storage or the network take their dependencies (UserDefaults,
clock, transport, sleep) as parameters, so they can be tested without an App Group or a
server. SwiftUI view bodies are not snapshot-tested.
Run the tests on macOS and on an iOS simulator:
swift test
xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro'Three iOS tests are skipped on purpose: they render the README images and run only through
scripts/render-screenshots.sh.
- Tag a release that matches the current code, so the package can be added by version.
- Snapshot tests for the view bodies.
- An example Live Activity in the example app.
The CHANGELOG records what changed so far, including how the package was
restored in October 2026 after the v1.0.0 code turned out not to build.
Contributions are welcome. Read the contributing guide and the code of conduct before opening a pull request. Report security issues privately as described in the security policy.
Released under the MIT license. See LICENSE.