A Simple Way to Debug SwiftUI Layout Issues

Published: (February 3, 2026 at 06:00 AM EST)
8 min read
Source: Dev.to

Source: Dev.to

Source: Dev.to

SwiftUI Layout Debugging Techniques

SwiftUI layout bugs are uniquely frustrating because the framework does so much for you that, when something goes wrong, the cause is often invisible. There’s no frame inspector like in UIKit, no red “misplaced view” warning—just a screen that looks wrong and no obvious way to ask why.

Below is a handful of simple, practical techniques that have saved me countless hours. None of them are fancy or conference‑talk material, but they work, and once you start using them you’ll wonder why you ever relied on trial and error.

1️⃣ Understand What You Are Actually Debugging

Before jumping into tricks, internalise what SwiftUI does under the hood. The layout system is a three‑step negotiation:

  1. Parent proposes a size to the child.
  2. Child decides its own size (based on the proposal, its content, or its own rules).
  3. Parent positions the child within its coordinate space.

Every layout bug is a breakdown in one of these steps: the proposal isn’t what you expected, the child chooses a surprising size, or the positioning is off.

The problem is that none of this is visible by default. The goal of the tricks below is to make SwiftUI show its work.

Trick 1 – The Debug Background

The simplest tool, and the one I reach for first: slap a semi‑transparent background on any view to see its actual bounds.

.background(Color.red.opacity(0.3))
  • Smaller than expected → the parent is constraining the view.
  • Larger than expected → the child is being greedy.
  • Size is right but position is off → you have an alignment issue.

Reusable modifier

extension View {
    func debugBorder(_ color: Color = .red) -> some View {
        self.border(color, width: 1)
    }
}

Usage

Text("First")
    .debugBorder(.red)

Text("Second")
    .debugBorder(.blue)

Now you can colour‑code sibling views and instantly see how they share space.

Trick 2 – Print the Proposed Size with GeometryReader (Without Breaking Layout)

A background shows the result of the layout negotiation, but sometimes you need the proposal that the parent offered.
Wrapping a view directly in GeometryReader is a common mistake because the reader expands to fill all available space, altering the layout you’re trying to debug.

Solution
Attach a GeometryReader as a background (or overlay) so it inherits the view’s size instead of greedily expanding.

.background(
    GeometryReader { geo in
        Color.clear.onAppear {
            print("Proposed size: \(geo.size)")
        }
    }
)
  • Color.clear occupies no visual space.
  • The GeometryReader reports the size of the view it decorates.

Use this technique when you suspect a parent (e.g., ScrollView, NavigationStack) is offering less space than you expect.

Trick 3 – The Overlay Size Label

Console prints are handy, but constantly switching between the simulator and Xcode’s console is tiring. Show the dimensions directly on the view:

extension View {
    func debugSize() -> some View {
        self.overlay(
            GeometryReader { geo in
                Text("\(Int(geo.size.width)) × \(Int(geo.size.height))")
                    .font(.caption2)
                    .foregroundColor(.white)
                    .padding(4)
                    .background(Color.black.opacity(0.7))
                    .cornerRadius(4)
            }
        )
    }
}
Image("avatar")
    .debugSize()

The overlay prints the view’s width × height in the corner, letting you spot spacing or sizing issues at a glance.

Trick 4 – Use .layoutPriority() to Understand Who Wins

A classic SwiftUI mystery: two Text views in an HStack, and one gets truncated even though there should be enough room. The culprit is usually layout priority. When space is insufficient, SwiftUI gives higher‑priority views their preferred size first.

HStack {
    Text("A really long title that matters")
        .layoutPriority(1)   // ← give this view priority
    Text("Details")
}
  • Increasing the priority of the first text tells SwiftUI: “I need my full size; the other view can shrink.”
  • By experimenting with different priority values you can quickly see which view is winning the space battle.
  • Even if you later remove the modifier, the act of adding it reveals the underlying conflict.

Trick 5 – Replace Views with Fixed‑Size Rectangles

When a layout is completely baffling and you can’t tell which view is causing the problem, strip everything down to coloured rectangles of known size. This isolates the layout logic from the content.

HStack(spacing: 12) {
    Rectangle()
        .fill(Color.blue.opacity(0.4))
        .frame(width: 80, height: 40)   // placeholder for a complex view

    Rectangle()
        .fill(Color.green.opacity(0.4))
        .frame(width: 120, height: 40)

    Rectangle()
        .fill(Color.orange.opacity(0.4))
        .frame(width: 60, height: 40)
}
.debugBorder(.red)   // optional: see the HStack’s bounds

Replace each problematic view with a rectangle of the size you expect it to be. If the overall layout still looks wrong, the issue lies in the container (spacing, alignment, padding, etc.). Once the container behaves correctly, swap the rectangles back for the real views one by one, checking after each replacement.

Putting It All Together

  1. Start with a debug background – add a colored background to see the actual bounds of the view.
  2. Print the proposed size – wrap the view in a GeometryReader and log its size (or display it) if the bounds look off.
  3. Overlay a size label – place a small Text overlay that shows the width × height for quick visual feedback.
  4. Play with .layoutPriority() – increase the priority of a view to discover which one is being starved of space.
  5. Isolate the container logic – if the problem persists, replace the content with fixed‑size rectangles (e.g., Color.red.frame(width: 50, height: 50)) to verify the container’s behavior.

These techniques are intentionally low‑tech; they rely only on SwiftUI’s built‑in primitives and a few one‑line extensions. Keep them in a handy snippet library, and you’ll spend far less time guessing and far more time building. Happy debugging!

Trick 5 – Replace Views with Colored Rectangles

k {
    Rectangle()
        .fill(.red)
        .frame(width: 100, height: 50)

    Rectangle()
        .fill(.blue)
        .frame(width: 100, height: 50)

    Rectangle()
        .fill(.green)
        .frame(width: 100, height: 50)
}

This removes all the complexity of text sizing, image intrinsic sizes, and content‑dependent behavior.
If the layout looks correct with the rectangles, the problem is not your structure; it is how a specific child view is sizing itself.

  1. Swap the rectangles back one at a time until the layout breaks.
  2. The view you just swapped in is the culprit.

This is essentially a binary‑search approach to debugging, applied to UI. It may feel crude, but it isolates problems quickly—especially in complex layouts where several elements might be interacting in unexpected ways.

Trick 6 – The .fixedSize() Test

This is more of a diagnostic question than a fix, but it is incredibly revealing.

If a view is not the size you expect, try adding .fixedSize() to it.
This tells the view to ignore the parent’s proposal and use its own ideal size.

What happens

  • The view suddenly becomes the size you wanted → the problem is upstream: the parent is constraining the child.
  • The view becomes ridiculously large → the child’s ideal size is unbounded, and you actually need a different parental constraint.

.fixedSize() also accepts parameters for specific axes:

.fixedSize(horizontal: true, vertical: false)

When to use it

It is especially useful for Text views that are:

  • Wrapping when you do not want them to, or
  • Not wrapping when you do want them to.

Adding .fixedSize(horizontal: true, vertical: false) tells the text “take all the horizontal space you want,” which reveals whether the wrapping is caused by an unknown width constraint.

Tip: Remove this modifier after debugging; the point is what it teaches you about the layout negotiation.

Trick 7 – Watch the Console for Layout Warnings

SwiftUI occasionally prints layout warnings. Many developers miss them or treat them as noise.

Common warnings

  • Unable to simultaneously satisfy constraints – more common in UIKit interop, but it can appear in SwiftUI.
  • Bound preference … tried to update multiple times per frame – often a sign of a layout feedback loop.
  • onChange(of:) action tried to update multiple times per frame – indicates your layout is oscillating.

The last one is particularly sneaky. It means a view’s size depends on some state that changes when the view resizes, which triggers another resize, and so on. SwiftUI catches the infinite loop and breaks it, but the layout still ends up wrong.

If you see these warnings, do not ignore them. They are SwiftUI telling you exactly what is broken.

Trick 8 – Use _printChanges() in the Body

When you suspect a view is re‑rendering too often and causing layout instability, drop this inside the view’s body:

var body: some View {
    let _ = Self._printChanges()
    // rest of your view
}
  • What it does: Prints a message to the console every time the view’s body is recomputed, along with why it changed (which @State, @Binding, or @ObservedObject triggered the update).
  • Why it helps: Layout bugs are often caused by unexpected re‑renders—e.g., a view’s size changes because its content changed due to a state update you didn’t anticipate.
  • How to interpret the output: If you see the view printing changes dozens of times per second, you’ve likely found a feedback loop, which is almost certainly related to your layout issue.

Putting It All Together

  1. Add debug backgrounds (or use .debugSize()) on the view that looks wrong and its immediate parent.

    • This reveals the actual sizes. In most cases the problem becomes obvious.
  2. Inspect the parent’s proposal with the GeometryReader‑as‑background trick.

    • If the proposed size is incorrect, the parent is the culprit.
  3. Isolate the offending view in a complex layout:

    • Replace views with colored rectangles, then swap them back one at a time until the issue reappears.
  4. Try .fixedSize() on the problematic view.

    • Fixes the size → the parent is constraining it.
    • Makes things worse → the child itself is the problem.
  5. Check the console for layout warnings and call _printChanges() to spot unexpected re‑renders.

This is a systematic process, not guesswork. Each step narrows the problem until the fix becomes obvious.

Summary

Next time your layout looks wrong, give yourself a few minutes with debug backgrounds and size overlays before changing a single line of layout code. You’ll be surprised how often the fix is obvious once you can actually see what is going on.

Back to Blog

Related posts

Read more »

[SUI] Barra de búsqueda

Barra de búsqueda en NavigationStack Un NavigationStack puede incluir una barra de búsqueda mediante el modificador searchable. Su firma es: swift searchable t...

[SUI] LabeledContent

LabeledContenthttps://developer.apple.com/documentation/swiftui/labeledcontent es un contenedor que adjunta una etiqueta a un control. Uso básico swift struct C...

[SUI] Formularios (Form)

Form Form es un contenedor para agrupar controles, principalmente usados para la configuración de alguna funcionalidad. Presenta los controles en un List con e...