---
title: "SwiftUI's Missing onLayout Modifier"
source: "/writing/swiftuis-missing-onlayout-modifier/"
---

Article

# SwiftUI's Missing onLayout Modifier

Paul Himes

March 9, 2021

On a recent SwiftUI project I ran into an issue while implementing a custom translucent title bar over a scroll view. I needed the content of the scroll view to land just below the the title bar and scroll behind it when the scroll view scrolls.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". The scrollable content is rubber band bouncing up and down with the content text coming to rest exactly below the title bar.

The Goal

In this article, we'll walk through how to build this common iOS UI design in SwiftUI. Along the way we'll also develop a powerful new view modifier which can help us implement this and other complex layouts.

Let's start with a simple SwiftUI view containing a `ScrollView` with a `TitleBar` overlaid on the top edge. Then we'll add some simple `Text` content so we can easily see how the views are being laid out.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". The scrollable content is rubber band bouncing up and down with the content text coming to rest exactly at the top of the screen.

Initial Layout

Now we need to make sure the scrollable content comes to rest at the bottom of the `TitleBar` rather than at the top of the screen. While this type of layout is easily accomplished with Auto Layout in UIKit, it's a bit more difficult in SwiftUI. SwiftUI's `ScrollView` type doesn't offer the same content inset configuration as `UIScrollView`. Instead, we can simulate the inset by adding a constant padding to the top of the `ScrollView`'s content.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". The scrollable content is rubber band bouncing up and down with the content text coming to rest exactly below the title bar.

Hard-Coded Padding

This looks exactly like what we want! Unfortunately, hard-coding the top padding isn't going to work because our `TitleBar`'s height may change at runtime. If the title text, font size, or screen orientation changes, the content could become misaligned with the bottom of the bar.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". Size of the text labels is shrinking and growing showing. When the text is small, the scroll view content doesn't align with the title bar.

Misaligned at Small Font Sizes

We need to communicate the height of the `TitleBar` to the `ScrollView` whenever the bar height changes. There isn't a mechanism in the SwiftUI layout system to connect the properties of sibling views in this way, but there are other ways to pass this information between the two views. One way it can be done is by using a shared `@State` variable.

The `ScrollView`'s top padding is now controlled by a property of the view. Next, we need to set that property to the height of the `TitleBar`. Reading the height of a view at runtime is done with a view called `GeometryReader`. This is a flexible view which takes whatever layout size its parent view offers and returns it in a closure. Since we don't want the `GeometryReader` to interfere with our `TitleBar`'s layout or view hierarchy, we'll place it in a `background` modifier on the `TitleBar` view. A view in a `background` modifier always assumes the size of its modified view.

Inside the `GeometryReader` we need to supply a content view. Again we don't want anything that will affect the layout or appearance of the view hierarchy, and there are several innocuous views we could choose from. In this case we'll use a clear `Color` view.

Now we can use the value of the `GeometryProxy` provided by the `GeometryReader` to update the `titleBarHeight` property of our `MainView`. To do this, we need a way to run arbitrary code within our view building code. One option is to add an action modifier, such as `onAppear`, to our `Color` view. Within the `onAppear` closure we can set the value of `titleBarHeight`.

If you run this code, you'll see that the `ScrollView` content now perfectly aligns with the bottom of the `TitleBar`. Unfortunately, the alignment is only correct as long as the size of the `TitleBar` doesn't change after appearing. If it does, the `ScrollView` content and `TitleBar` will once again be misaligned. This is because the `onAppear` modifier only updates the `titleBarHeight` once, when the view appears.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". Size of the text labels is shrinking and growing showing. When the text is small, the scroll view content doesn't align with the title bar.

Still Misaligned When Font Size Changes

At this point we have two options. The first is to add more action modifiers to our `Color` view. There are actions that fire in most, but not all, situations which might cause the `TitleBar` height to change. The second option is to abuse SwiftUI's layout system to run arbitrary code every time the `TitleBar`'s layout changes. We're going with option two.

Every time our UI layout changes, SwiftUI requests the `body` computed property of our views. Normally this property only contains code to build and return a nested hierarchy of views. However, it is possible to include additional statements alongside the view code. For example, we could set the `titleBarHeight` to the height value from the `GeometryProxy` directly, outside of any action modifier closure.

This causes two new problems. The first is that the Swift compiler can no longer determine what type of view is contained by our `GeometryReader`.

The block of code including the GeometryReader. A build error on the first line says, "Unable to infer complex closure return type."

The Compiler Isn't Expecting Arbitrary Code

To alleviate this issue, we can extract the content view into a helper function which takes a `GeometryProxy` parameter and returns a type of `some View`.

The second problem can be seen when you try to run this code.

The block of code including the layoutWatcher function. A runtime issue on the first line where the title bar height is set says, "Modifying state during view update, this will cause undefined behavior."

Xcode is Always Watching Out For Us

Xcode generates a runtime issue to notify us that we shouldn't modify state during a view update. This issue can be silenced by waiting to change the state until after the view update. Here we'll dispatch the state update asynchronously to the `main` queue. This will allow the view update to complete before modifying the `titleBarHeight` value.

This is still slightly risky and could result in continuous, uncontrolled view layout updates. If, for example, changing the `titleBarHeight` somehow caused the `TitleBar` view to update, we might end up in an infinite loop. In our case, this isn't a problem. When the `TitleBar` height changes, only the `ScrollView` padding is changed. This does not affect the layout of the `TitleBar`.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". Size of the text labels is shrinking and growing showing. When the font size changes, the scroll view content remains aligned with the title bar.

It Works!

At this point we have a fully functional implementation of a scrollable view with proper padding to avoid a translucent title bar when the view is scrolled all the way to the top. We could stop now, but wouldn't it be nice to refactor this into a general solution that's reusable in other situations? Let's create a new `ViewModifier`, similar to `onAppear`, which will let us react to the current size of any view whenever its layout changes.

Most of the code in this `ViewModifier` comes from our previous implementation. The major difference is, instead of setting the value of `titleBarHeight`, it takes an action closure which is given a copy of the `GeometryProxy` for the modified view. It's also conventional to extend `View` with a convenience function to apply the `ViewModifier`. Our function is designed to work similarly to the `onAppear` modifier. Here's our final view implementation using our new `onLayout` `ViewModifier`.

`onLayout` is a powerful tool for developing and debugging complex SwiftUI views. Just remember to avoid changing state which would affect the layout of the view you're observing. I hope you find it useful.

An animation of an iOS UI. At the top is a translucent purple title bar with the text "Title". Behind and below is a blue scrollable content area with the text "ScrollView Content". The scrollable content is rubber band bouncing up and down with the content text coming to rest exactly below the title bar.

The Final Product

If you've discovered other ways to achieve this effect, I'd love to hear about them in the comments!

_Paul works at_ [_Livefront_](https://livefront.com/) _, where the test coverage is strong, the designs are good looking, and all the developers are above average._

-   Paul Himes

    Software Engineer

    [More from Paul](/team/paul-himes/)

    Paul Himes

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
