# One Layout Engine for the App's Content Lists

> How GoodBarber's list templates got one shared layout: the same code sizes and places a list on the home page and on a full screen, measures each item with the view that draws it, and slots native ads between items.

- Context: GoodBarber · Platform
- Published: 2026-07-01 · Updated: 2026-09-29
- Reading time: 8 min
- Canonical: https://lanfranchistephane.dev/articles/layout-engine/

## System at a glance

1. Template — The list design the owner picked
2. Content or ad — What goes in each place
3. Measure — Sized by the view that draws it
4. Place — Same frames, home or full screen
5. Draw — Reused cells, updated only on change

## At a glance

- Role: iOS engineer who designed the shared layout and its two adapters, wrote the first layouts, and added native ads.
- Challenge: Give each list template one set of sizing and placement rules for the home page and for full screens, without rewriting the home page or the older widgets on it.
- Outcome: Ten home widget templates and sixteen kinds of full-screen section get their sizes and positions from shared layouts, and three home lists can show native ads.

## What was at stake

### A list template had to size and place its items the same way wherever it appeared.

GoodBarber builds thousands of iOS apps from one engine. App owners show their content as lists, such as articles, videos, podcasts, events, or places, and pick a template for each list: a condensed list, cards, a grid, a carousel, a story.

The same template can appear in two places: as a block on the app's home page, which GoodBarber calls a widget, and as a full screen of its own, called a section. Before this work, a template that lived in both places was built twice. On the home page, each widget computed its own sizes and positions. On a full screen, the section started from an estimated height and let each cell size itself. Every new template meant writing that code twice.

**My responsibility — the layout engine.** I designed the shared layout and the two adapters that connect it to the home page and to full-screen sections, wrote the first layouts, and added native ads. The list cells predate this work; I made them skip updates that change nothing. Teammates built the height cache and many of the later templates.

## One layout, two places

A layout is one object per template. It answers every question a list asks. How many cells are there, and which kind goes in each place? How tall is each item, and where does each cell sit? Does the list scroll down, sideways, or page by page? It also describes the list's own look: background, separators, shadow, and corners.

A layout does not know whether it sits on the home page or on its own screen. Two adapters take care of that.

<picture>
  <source media="(max-width: 720px)" srcset="https://lanfranchistephane.dev/articles/layout-engine/one-layout-two-places-narrow.svg">
  <img src="https://lanfranchistephane.dev/articles/layout-engine/one-layout-two-places.svg" width="960" height="364" loading="lazy" decoding="async" alt="The template the app owner picked and the list's content go into one layout written for that template. The layout decides what goes in each place, how tall each item is, where each cell sits, and how the list scrolls. One adapter hands its sizes and positions to the older home page code; a second adapter hands the same sizes and positions to UIKit on a full screen.">
</picture>

Once the engine existed, the team built new templates on it, such as cards, grids, and stories, for articles, videos, podcasts, places, and photos. Each new template is one layout, not two sets of sizing code.

## 1. Decide what goes in each place

Most places hold an item from the list. On the home page, three list templates can also show native ads: ads from Google AdMob, drawn in the list's own style. The app owner chooses where the first ad goes and how often the next ones follow.

- An ad never takes the first place.
- Only ads that have arrived take a place, in order. A planned place with no ad yet does not appear, so there is no empty gap.
- The ad cell uses the list's fonts, corners, borders, and padding. It shows the advertiser, the headline, an optional line of text, the ad's icon, its button, and Google's AdChoices mark.
- Ads never load for a reader under age, and never show when the app's ads are turned off, or for subscribers when the owner removed ads for them.

Ads push the articles further down the list, so a place number is no longer an article number. The layout works out what each place holds before it measures, draws, or opens anything.

<picture>
  <source media="(max-width: 720px)" srcset="https://lanfranchistephane.dev/articles/layout-engine/ad-places-narrow.svg">
  <img src="https://lanfranchistephane.dev/articles/layout-engine/ad-places.svg" width="960" height="320" loading="lazy" decoding="async" alt="The app owner puts the first ad at place 3, then one every third place, and two ads have arrived. Five articles and two ads fill seven places: article 1, article 2, an ad, article 3, article 4, an ad, article 5. Tapping place 4 opens article 3.">
</picture>

```pseudo title="What goes in each place"
if an ad has arrived for this place:
  show the ad
otherwise:
  count the ad places before this one
  the article is this place minus that count
  size it, show it, and open it on a tap, all from that same article
```

The same lookup serves the height, the content, and the tap, so a cell always opens the article it shows. A tap on an ad is left to Google's ad view: the engine opens nothing.

## 2. Measure with the view that draws it

Each layout keeps a hidden cell of the kind the place needs. To find how tall an item is, it gives that hidden cell the same content and design the visible cell will get, only without the tap handlers, sets the width, and reads the height. Ads are measured the same way, with the ad cell. Because the same drawing code measures and displays, a title that wraps to three lines on screen was also measured on three lines.

Measuring a real view is not free, so heights are remembered. The key is the section or widget, the width of the list, and a fingerprint of the item: its identifier, title, summary, date, comment count, and a few more fields. Rotate the phone and the width changes, so items are measured again. Edit an article's title and its fingerprint changes, so it is measured again too. The remembered heights last as long as the app runs, or until its cache is cleared.

```pseudo title="Size and place one cell"
look for a remembered height for this section, this width, and this item
if there is none:
  give the hidden cell the item's content and the list's design, at this width
  read its height and remember it
in a grid, give every cell in a row the height of the tallest one
place the row below the previous row, plus the spacing
```

Positions come from the heights. The first row starts at the list's top padding, and each next row starts below the previous one. Positions are remembered too, but only while the list is being laid out: they are thrown away at the end of each pass, so a position from an earlier pass is never reused.

## 3. Place the cells on the home page or a full screen

**The home page** is older code. It expects each block to be a widget object that reports its cells, its size, the frame of each cell, its scrolling and margins, and what a tap does. The first adapter wraps a layout in such an object, so the home page did not change. Older widgets, which still measure themselves, keep running next to the new ones on the same page.

**Full-screen sections** use UIKit's compositional layout, Apple's building blocks for collection views (the scrolling lists and grids of iOS). The second adapter takes the width UIKit offers, removes the screen's margins, asks the layout for the frame of every cell, and hands those frames to UIKit as they are. It adds the list's background, separators, and shadow as separate views around the cells. When the list is empty, or UIKit has no width yet, it builds nothing rather than a list of zero size.

Both adapters get their frames from the same layout, so a template follows the same sizing and placement rules in both places.

## 4. Draw only what changed

Cells are UIKit's reusable cells: a cell that scrolls off screen comes back for another item. Each template's cell takes one value, a content configuration (UIKit's plain description of what a cell shows), that holds the item's content and the list's design.

When it receives a new value, the cell compares it with the one it has:

- **Nothing changed:** it does nothing.
- **Only the content changed:** it sets the new text, media, and buttons, and keeps its fonts, colors, and shape.
- **The design changed:** it restyles itself, then sets the content.

```pseudo title="Update a cell"
compare the new value with the current one, then keep the new one
if the design changed:
  apply fonts, colors, spacing, and shape
if anything changed:
  set the text, media, and buttons
lay out once, at the end
```

Setting the content covers both cases for every optional part: the thumbnail is shown when the item has one and hidden when it does not, and the same goes for the player, the ticket button, and the actions menu. Nothing is left over from the previous item. However many parts change, the cell lays itself out once, at the end.

## When things change

| What happens | What the engine does |
|---|---|
| The phone rotates | The list's width changes, so items are measured again at the new width |
| An article's title is edited | Its fingerprint changes, so it is measured again |
| No ad has arrived yet for a place | That place is skipped, so the list has no empty gap |
| The reader subscribes, and the owner removed ads for subscribers | No ad places are made |
| A reader taps an ad | The engine opens nothing; Google's ad view handles the tap |
| A full-screen list is empty, or UIKit has no width yet | The adapter builds nothing |

## Trade-offs

- **Adapters instead of a rewrite.** The home page still talks to widgets the old way, and an adapter translates. Older widgets keep their own sizing code, so two systems live side by side until the rest move over.
- **Exact heights cost a real view.** Measuring with a hidden cell gives the true height, but building and laying it out takes time on the main thread. Remembered heights make it a one-time cost per item and width.
- **Native ads reach the home page only.** Full-screen sections do not show them yet.
- **Built in 2025 on UIKit**, with content configurations for the cells and compositional layout for full screens.

## Related

- [Decoding Images at the Size They Are Shown](https://lanfranchistephane.dev/articles/image-memory/) — GoodBarber · Platform, 6 min read. Why small thumbnails filled memory in GoodBarber apps, and how the shared image loader now decodes each image at the size it appears on screen, while every existing screen keeps loading images as before.
- [One Subscription Flow for Thousands of Apps](https://lanfranchistephane.dev/articles/in-app-purchase-infrastructure/) — GoodBarber · Platform, 6 min read. How GoodBarber apps sell subscriptions: one shared service shows only what Apple can sell, lets the server confirm every purchase, and unlocks the same access on every screen.
