Home / Blog / WidgetKit: the real design rules for Home Screen and Lock Screen

WidgetKit: the real design rules for Home Screen and Lock Screen

Widget design isn't app design. I've shipped widgets in three apps and learned something new each time. Timeline, relevance, and sizing calls.

Widgets shipped with iOS 14 and moved to the Lock Screen in iOS 16. I’ve added widgets to three of my apps, and each one taught me something new. The biggest lesson: a widget is not an app, and it doesn’t play by app rules.

Here’s how I think about widget design, the timeline provider, and relevance scores.

A widget is not an app

The most common beginner mistake is treating a widget like a mini app. A widget isn’t tappable (except for a single deep link on Medium and Large), doesn’t scroll, and doesn’t refresh in real time. Think of it as a photo.

A widget is a snapshot. Apple renders a visual snapshot every few minutes and displays it on the home screen. Tapping opens your app. The widget itself has no interactivity (until iOS 17).

iOS 17 added interactive widgets: Button and Toggle work. Even then, it’s limited, you can’t drive complex interaction from a widget.

How the timeline provider works

The heart of a widget is the TimelineProvider protocol. Three methods:

placeholder(), the static shell iOS shows while the widget is loading. No real data, just a UI skeleton.

getSnapshot(), the preview rendered in the widget gallery. Fast to render with fake or demo data.

getTimeline(), where the real work happens. You return a list of entries, each with a date, and iOS renders the widget with the right entry at each time.

A sample timeline:

let now = Date()
let entries = [
    SimpleEntry(date: now, data: currentData),
    SimpleEntry(date: now.addingTimeInterval(3600), data: nextHourData),
    SimpleEntry(date: now.addingTimeInterval(7200), data: twoHoursData),
]
let timeline = Timeline(entries: entries, policy: .atEnd)

policy: .atEnd, ask for a new timeline after the last entry expires.
policy: .after(date), ask for a new timeline at a specific date.
policy: .never, don’t ask again until a manual reload.

How often does a widget refresh?

This catches everyone off guard: a widget has no refresh frequency of its own. It refreshes when your timeline says it should.

But Apple puts limits on it:

  • Typical: a refresh every 5 to 10 minutes is plenty. News, weather, that kind of thing.
  • Energy budget: Apple gives each widget a daily refresh budget. Ask too often and the system ignores you.
  • Real-time is off the table. You can’t build a widget that shows live prices or live scores second by second.

The best strategy: pre-generate entries an hour ahead and let iOS render them. Minimise your refresh budget usage.

Widget sizes and design

iOS widgets come in three main sizes:

systemSmall: a 1×1 tile, about 158x158pt. One piece of information or a simple stat. Tap goes to a single deep link.

systemMedium: a 2×2 layout. A bit richer. Still a single tap target.

systemLarge: a 2×4 layout. Most detail. Multiple tappable areas on iOS 17+ with SwiftUI Link.

Lock Screen widgets on iOS 16+:

accessoryCircular: a ring, one metric. Think “ring fill”.

accessoryRectangular: short, one line of information.

accessoryInline: a single line of text in the clock bar.

In Dentii I support four widget sizes. Each one has a distinct use case:

  • Small: today’s brushing status (done or not done)
  • Medium: the last seven days’ consistency chart
  • Circular (lock screen): current daily streak count
  • Rectangular (lock screen): “time to brush tonight” reminder

Relevance score (Smart Stack)

iOS Smart Stack rotates widgets automatically. Which one bubbles to the top? Relevance score decides.

If you add a relevance to the timeline entry, iOS uses it:

let entry = SimpleEntry(
    date: Date(),
    data: currentData,
    relevance: TimelineEntryRelevance(score: 0.8, duration: 600)
)

score: between 0 and 1. 1 means very important.
duration: how many seconds the score stays valid.

In Snoozio the sleep reminder widget’s relevance climbs toward night (0.3 to 0.9). Smart Stack pushes it to the top around 10pm.

When a user taps a widget, Smart Stack “learns” from it and shows it more often in similar contexts.

Deep link handling

Tapping a widget should drop the user in the right place in the app. In the widget you use SwiftUI Link:

Link(destination: URL(string: "myapp://dashboard")!) {
    Text("Dashboard")
}

On the app side you handle the URL scheme:

.onOpenURL { url in
    if url.host == "dashboard" {
        navigationPath.append(.dashboard)
    }
}

iOS 17+ lets you put a Button inside a widget, but only through AppIntent. Link is usually simpler.

Design discipline

Rules I follow for widget design:

1. Focus on one piece of information. Cram five things into a small widget and none of them read.

2. Keep text large. 14pt minimum, 16 to 18pt is ideal. Smaller text is unreadable.

3. Push contrast hard. Every user has a different wallpaper on the home screen. Low background opacity plus strong text contrast.

4. Don’t forget loading and error states. Have placeholders for “loading” and “offline”. An empty widget is a bad experience.

5. Test dark mode. It should look good against both light and dark wallpapers.

What widgets can’t do

Things you often have to explain to users:

  • No real-time data. You’re looking at a snapshot that’s up to five minutes old.
  • Network calls are async. No synchronous network on widget load.
  • Keychain access is limited. The widget runs in a separate process, so you share the app’s keychain via an app group.
  • Shared data lives in an App Group. UserDefaults, Core Data, and file-system sharing all need an App Group.

Testing

Widget testing is easy on the simulator but more honest on a real device. My strategy:

  1. Widget gallery preview. What you see when first adding the widget.
  2. A design preview for each size. Small, medium, large, check all of them.
  3. Lock Screen widgets on iOS 16+ real devices.
  4. Dark mode toggle and multiple wallpapers.
  5. Deep link: tapping the widget lands on the right screen?
  6. Refresh testing: watch it over 24 hours and confirm the timeline refreshes as expected.

Takeaway

A widget is the “always visible” arm of your app. Designed well, it gives the user value every time they glance at the home screen, without opening the app. But it isn’t an app, so the rules change: static, periodic, single-focus.

On your first widget project start small. Small size, one piece of data, a one-hour refresh interval. Expand from there as you need to. Building a complex widget is easy. Building a good widget is hard.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch