Skip to main content
BlogUncategorized

The iOS widget memory limit is the real design constraint

By August 20, 2026No Comments

A home screen widget on iOS is not a small version of your app. It is a separate process with its own hard memory ceiling, and the fastest way to find that ceiling is to cache a photo at the wrong size. Pulso, my watch collection app, now has six widget faces on the home screen, and most of the bugs worth writing down were about that gap between a small view and a small app.

The rule I set before any of it was built: no widget can ever spend a lookup. Pulso charges credits for price research, and a home screen refresh nobody asked for should never cost a user money. So the widgets read a snapshot the app writes into a shared App Group, and they have no route to the paid endpoints at all. That is a product decision enforced by architecture rather than by discipline, which is the only kind that survives a refactor.

The memory ceiling is the constraint, not the layout. The widgets shipped and then showed the placeholder forever, on device, with nothing useful to read anywhere. The cause was UIGraphicsImageRenderer, which defaults to screen scale, so watch photos meant to be cached at 400px were written at 1200px instead. About 34MB of images, handed to an extension allowed a fraction of that. The app code was correct. The widget was being killed before it could draw.

scaledToFill reports the photo, not the frame. The four tile face overflowed its own bounds, and I misdiagnosed it as margins three separate times, because margins are what an overflowing layout looks like. scaledToFill tells the layout system the size of the source image rather than the size of the box you put it in, so four large photos push the container outward. Color.clear with the image as an overlay fixes it, because the clear rectangle is then the thing reporting its size.

containerBackground exists for a reason. Painting the background in the view body produces a box inside a box, since the system adds its own margins around whatever you return. The background belongs in containerBackground. This is documented, and I still built it wrong first.

Empty space is a bug with a number attached. The faces were between 59% and 73% filled, with single voids of 152 to 182 points. On a real phone that reads as an unfinished widget rather than a minimal one. Redistributing the content brought every void under 26 points, and the spec icons the app already shows on each watch card, movement, case size, water resistance and power reserve, now appear on the widgets too, so a face carries the same facts as the row it came from.

A scrim is tuned to a dial. The gradient sitting behind the text was tuned against a black dial and was illegible over a bright orange one, which is the kind of thing you only find by putting somebody else’s watch in the frame.

The one genuinely interactive piece is the wear button on Today’s Watch and on the four tile picker. Tapping it logs a wear without opening the app, writing to an outbox and updating the snapshot optimistically, so the widget changes immediately and reconciles afterwards. That was verified end to end rather than assumed, because an optimistic update that never reconciles is just a lie with good timing.

What I will not claim yet is that the snapshot layer is efficient. It is correct, it refuses to spend money, and it redraws when it should. Whether it redraws more often than it needs to is a question I have not measured, and I would rather say that plainly than round it up to done.

[SCREENSHOT: the six Pulso widget faces on a home screen, light and dark]

Leave a Reply