The workshop
On my home screen

Photo Widget

My photo, my crop, my home screen.

Apple’s photo widget won’t let me pick the one picture I want, and the App Store alternatives come with ads or a paywall. So I built my own: my photos, my crop, on a schedule I choose.

Nothing is uploaded, ever.
Role
Design & development, solo
Built for
My own home screen
Built
June 2026 · rebuilt in September
Stack
Swift · SwiftUI · WidgetKit

01Why it exists

I wanted one picture on my home screen. Framed the way I’d frame it.

That’s really all this is: putting an image on my iPhone’s home screen. Apple’s own photo widget wouldn’t let me choose what it shows. I couldn’t give it just one picture — it had to be several — and in my opinion it just isn’t intuitive.

I didn’t want to go shopping on the App Store either. I just know some of those apps come with a payment, ads, or something else not to my liking.

So I made it myself. The first version, in June, did exactly one thing: put one photo of my choosing on the home screen.

In September I rebuilt it as a proper app. Now there can be several widgets, each with up to 50 photos that rotate on a schedule or shuffle, with filters, frames and captions. And it has no network code at all — nothing leaves the phone.

02Try it

One crop. Every shape.

A crop isn’t saved as a rectangle. It’s a centre point and a zoom, and each widget shape is cut from that pair when it’s drawn. Drag the photo — or any widget on the phone — and all three follow.

The photo

Large
Small
Medium

Arrow keys move the centre; hold Shift for bigger steps. Plus and minus zoom.

1.0×
CropState(centerX: 0.50, centerY: 0.50, zoom: 1.00)
Frame
Mat
Caption

Drag a widget, or double-click to zoom.

Interactive illustration of the crop model, rebuilt for the web: the projection, clamping, rubber-banding and centre-snapping follow the app’s Swift. In the app each widget size can also keep a crop of its own; here all three share one so you can watch the projection. The starting points stand in for Vision’s, and the photos are mine.

03Timing

Every photo once, before any repeats.

The app and the widget never talk to each other. Both run the same pure function — the slot on the clock, plus a seeded shuffle — and get the same answer. Below is that function, ported line for line, planning a five-photo widget from now on.

Tap the widget to skip ahead, like the real one.

Change every
Order

Computed live in your browser, in your time zone, with the app’s own PhotoSchedule — SplitMix64 and all — for a demo widget with five of my photos. Each one is cropped the way you left it above.

04What it does

Small app. No catch.

As many widgets as I like

Each widget on the home screen picks which one it shows from the Edit Widget menu, with thumbnails. One photo is still the simplest case; a widget can hold up to 50.

Smart Frame, then my hands

Photos arrive framed by Vision — faces first, then whatever stands out. From there: drag, pinch, double-tap, or a 1–5× slider, per photo and per size.

On a schedule

Every 15 or 30 minutes, hourly, every 3 or 6 hours, daily, or never — in order or shuffled. Tap the widget to move on to the next photo.

Filters, frames, captions

Ten Core Image filters with an intensity slider. Full Bleed, Border or Instant frames on six mats, two coloured from the photo. Captions: my own text, the date it was taken, or “time ago”, in five fonts.

Part of the system

A Shuffle Photos control in Control Center, a Siri shortcut, deep links, tinted Home Screen support, and an “On Home Screen” badge read from WidgetCenter.

Nothing leaves the phone

No StoreKit, no network code, no analytics — so no ads, no subscription and no account. The app says it plainly: “Your photos never leave your device.”

05How it’s built

The app does the work. The widget just shows it.

About 4,400 lines of Swift, designed for iOS 26’s Liquid Glass. It runs on my phone straight from Xcode — it isn’t on the App Store.

AppSwiftUI with Liquid Glass controls, Observation for state, PhotosUI for picking photos.
WidgetWidgetKit timelines, with App Intents for choosing a widget, tap-to-advance, the Control Center control and the Siri shortcut.
ImagesImageIO decodes, Vision frames, Core Image filters. Every photo is pre-rendered for every size into the shared App Group.
PipelineEdits re-render after a 350 ms pause, skip anything a render manifest says hasn’t changed, then reload the widget’s timelines. Writes are atomic, so the widget never reads half a file.
Tests12 Swift Testing tests: scheduling, shuffle fairness, crop maths and JSON round-trips.
PhotoSchedule.swift
/// Decides which photo a widget shows at any moment. Pure and deterministic, so the app's
/// previews and the widget extension always agree without sharing any runtime state.
nonisolated enum PhotoSchedule {

    static func index(for config: PhotoWidgetConfig, at date: Date, offset: Int = 0) -> Int {
        let count = config.photos.count
        guard count > 1 else { return 0 }

        // A widget that never rotates stays on its cover until tapped.
        guard config.rotation != .never else { return mod(offset, count) }

        let step = slot(at: date, interval: config.rotation) + offset
        switch config.order {
        case .inOrder:
            return mod(step, count)
        case .shuffle:
            let cycle = floorDiv(step, count)
            return shuffled(count: count, seed: seed(for: config.id), cycle: cycle)[mod(step, count)]
        }
    }

06Build notes

The parts you don’t see.

01

A widget gets very little memory

Widget extensions run on a tight memory budget, so the heavy work — decoding, cropping, Lanczos scaling, filters — happens in the app, which pre-renders a finished JPEG for each photo and size. The widget only ever opens one small file, falling back to a thumbnail, then the photo’s average colour. Import decodes once, through an ImageIO thumbnail, to a 2,560 px master, a 540 px thumb and a 120 px blurred glow.

02

One crop that fits every shape

A crop is stored as a centre point and a zoom, not a fixed rectangle, so it projects onto a small square, a wide medium or a tall large — and changing the frame style never loses the framing. Vision picks the starting point.

03

App and widget agree without talking

Both work out the current photo with the same pure function: clock-aligned time slots plus a seeded shuffle (SplitMix64) that shows every photo once per cycle and never repeats one across a cycle boundary. It’s the code above.

04

A crop that feels right under a finger

Past the edges it rubber-bands on the same curve as UIScrollView. Near the middle it snaps to centre with a haptic tick, and pinch-zoom stays anchored under the fingers.