Skip to content

About

Measured layout annotations for SwiftUI previews: padding, spacing, sizes, corner radii and safe area.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

KeylineKit: measured layout annotations for SwiftUI previews

Swift versions supported, on the Swift Package Index Platforms supported, on the Swift Package Index Swift Package Manager compatible MIT license

KeylineKit

See the layout SwiftUI actually renders.

Is that padding 16 or 20? Where does the safe area end? Why is there a gap between those two rows? You usually find out by adding .border(.red), squinting at the Canvas and removing it again.

KeylineKit draws the answer on top of your preview. Wrap a view in #Keyline and every layer gets its name, its padding and the spacing between its siblings, measured in points from the rendered layout. It can also show sizes, corner radii and the device safe area.

KeylineKit website

🌟 Features · 🤔 Why KeylineKit? · 📲 Installation · 🐒 Usage · 🎨 Annotations · 🧩 Custom views · ⚙️ How it works · ❤️ Contributing

A SwiftUI account screen with every layer named, padding bands in orange and stack spacing in purple
Layers, padding and spacing
The same screen with the top and bottom safe-area insets in orange and the safe area outlined in green
Safe area
A profile card with width and height dimension lines and its corner radius
Corners and sizes

🌟 Features

  • 📏 Measured, not guessed: padding, spacing and sizes come from the rendered layout, in points.
  • 🏷 Every layer named: each view's type next to its bounds, nested containers included.
  • 📱 Safe area at a glance: device insets, the safe area and, on iOS 27.1, the reserved regions that cover content, such as camera housings.
  • ⭕️ Corner radii: the arc and value of cornerRadius: and .cornerRadius(_:).
  • 🧩 Your own views too: @Keylined opens a custom view's body to the overlay.
  • 🙈 No label pile-ups: all labels are laid out together, so they never cover each other.
  • 🌗 Light and dark: a palette tuned for both, or your own colors with .keylineColors(_:).
  • 🪶 Preview-only: #Keyline lives in your previews; your views render exactly as before everywhere else.

🤔 Why not just use .border()?

You can! A border or a background is often the quickest way to check one view. KeylineKit is for when you want the whole picture, with numbers:

You want to… Usually With KeylineKit
🔲 See a view's bounds Add .border(.red) to each view, then remove it Every layer is outlined and named
📐 Check padding Read the code and guess what default means Each edge is measured: 16 · default
↕️ Check stack spacing Add backgrounds or run the View Debugger The gap between siblings is filled and measured
📱 Check the safe area Print safeAreaInsets Insets and the safe area are drawn on the Canvas
⚡️ Stay in Previews The View Debugger needs a running app Works in #Preview, no app launch needed

📲 Installation

Build with Xcode 16 or later (Swift 6). Your app can target iOS 16+, macOS 13+ or watchOS 9+.

Capability Minimum
Layers, padding, spacing, sizes and corners iOS 16 / macOS 13 / watchOS 9
Safe-area insets iOS 16
Reserved regions that cover content iOS 27.1 SDK and runtime

In Xcode, choose File › Add Package Dependencies… and enter the repository URL. Or add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/HelioMartin/KeylineKit.git", from: "0.1.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "KeylineKit", package: "KeylineKit"),
    ]),
]

KeylineKit depends only on swift-syntax (600.0.0..<700.0.0), which its macros need.

🐒 Usage

Three steps:

1️⃣ Import

import KeylineKit

2️⃣ Wrap your preview

#Preview {
    #Keyline {
        VStack(spacing: 16) {
            ProfileView()
        }
        .padding(24)
    }
}

3️⃣ Read the Canvas

Every layer gets its name and outline. Padding appears as orange bands with a value on each edge, and the gaps between stack children appear in purple.

That's it! There is nothing to remove before shipping: #Keyline only runs where you write it.

💡 Values are measured, so they show what SwiftUI applies. When the source says something different, the declared value is shown next to it: .padding() reads 16 · default, and VStack(spacing: Theme.spacing) reads 12 · Theme.spacing.

🎨 Choose your annotations

With no arguments you get .layers, .padding and .spacing. Pass the ones you want instead:

#Keyline(.padding, .corners) { ProfileView() }
#Keyline(.safeArea, .layers) { AccountScreen() }
#Keyline(.layers, showContainerLines: false) { ProfileView() }
Annotation Draws Color
.layers The name of each layer next to its bounds #1447E6
.padding A band per .padding(...) call, a value per edge and the content outline #CA3500 #008236
.spacing The gap between siblings of a VStack or HStack #8200DB
.size Width and height dimension lines for each layer #314158
.corners The arc and value of each corner radius #C6005C
.safeArea Device insets, the safe area and reserved regions #CA3500 #008236 #8200DB
.none Nothing; outlines still follow showContainerLines

Layer outlines are on by default. Turn them off with showContainerLines: false.

🖍 Custom colors

Pass your own colors with .keylineColors(_:). Set only the ones you want to change; the rest keep the palette above:

#Preview {
    #Keyline(.padding, .safeArea) {
        AccountScreen()
    }
    .keylineColors(KeylineColors(padding: .red, content: .blue))
}
Color Draws
layers Layer outlines and names
padding Padding bands and their values, and the safe-area insets
content The content outline of each padding band, and the safe area
spacing The gap between siblings of a VStack or HStack
reserved Reserved regions that cover content
corners The arc and value of each corner radius
size Width and height dimension lines

A color draws lines and badges, and a translucent copy fills regions. Badge text stays white, so pick colors dark enough to read it. A dynamic color, such as one from your asset catalog, adapts to light and dark mode.

Applied to any parent view, the colors reach every #Keyline inside it. Nested calls combine, and the one closest to #Keyline wins.

🧩 Look inside your own views

#Keyline only sees the code inside its own closure, so a custom view like ProfileView() shows up as a single layer. Add @Keylined to the type and the overlay also draws what its body declares:

@Keylined
struct ProfileView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Image(systemName: "person.crop.circle.fill")
            Text("Taylor Morgan")
        }
        .padding(24)
    }
}

When a view and the root of its body share the same frame, they share a label: ProfileView › VStack.

🔍 What does the macro instrument?
  • Any capitalized initializer (VStack, ScrollView, NavigationStack, CustomCard…) is entered, including closures such as label:, header: and footer:.
  • The branches of if, switch and #if are entered.
  • ForEach and Group without modifiers are transparent: their children count as siblings of the enclosing stack.
  • Closures that are actions or don't build views are left alone: Button("Save") { save() }, TextField, Canvas, Chart, Map and Table. Button(action:) { … } and Button { … } label: { … } still instrument their label.
  • Literals that Xcode rewrites for live editing (__designTimeInteger(…)) are shown with their value.
⚠️ Current limitations
  • .corners reads the radius from modifiers, not from a root shape such as RoundedRectangle(cornerRadius:) used directly as a view.
  • A custom view with modifiers (ProfileView().padding()) is drawn as a single layer, even when it uses @Keylined.
  • Reserved regions need the iOS 27.1 SDK at build time and iOS 27.1 at runtime.

⚙️ How it works

flowchart LR
    A["#35;Keyline { … }"] -->|compile time| B["Each view wrapped in a node,<br/>probes around .padding"]
    B -->|runtime| C["onGeometryChange<br/>reports frames"]
    C --> D["Annotation layout:<br/>insets, gaps, labels"]
    D --> E["One Canvas overlay"]
Loading
  1. Macro (Sources/KeylineKitMacros): rewrites the closure at compile time, wrapping each view and inserting probes before and after every .padding(...) and after every modifier with a corner radius.
  2. Measurement (Sources/KeylineKit/Measurement): each node and probe reports its global frame with onGeometryChange. This also works inside NavigationStack and ScrollView.
  3. Drawing (Sources/KeylineKit/Rendering): a pure, tested layout step turns the frames into regions, dimension lines and non-overlapping labels, and a single Canvas draws them.

Types prefixed with _ are public only because the expanded code uses them; they are hidden from the documentation.

📱 Try the demo

Examples/KeylineDemo/KeylineDemo.xcodeproj has a #Preview for each annotation. The iOS app can also show the annotated screens at runtime:

xcrun simctl launch booted com.keylinekit.demo.ios -KeylineOverlay safeArea

Modes: defaults, safeArea and details.

❤️ Contributing

Found a layout KeylineKit draws wrong? I'd love to see it.

  • Open an issue with a small view that reproduces it.
  • Suggest an improvement or send a pull request.
  • Add a test for a fix: macro expansions live in Tests/KeylineKitMacrosTests and layout logic in Tests/KeylineKitTests.

Run the tests with:

swift test

📣 If xcode-select points to the Command Line Tools, SwiftPM can't find the SDK's SwiftUI macros (@State, @Entry). Use Xcode's toolchain: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test.

👨🏻‍💻 Author

Made by Helio Martín.

👮🏻 License

KeylineKit is available under the MIT license.

About

Measured layout annotations for SwiftUI previews: padding, spacing, sizes, corner radii and safe area.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages