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.
🌟 Features · 🤔 Why KeylineKit? · 📲 Installation · 🐒 Usage · 🎨 Annotations · 🧩 Custom views · ⚙️ How it works · ❤️ Contributing
Layers, padding and spacing |
Safe area |
Corners and sizes |
- 📏 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:
@Keylinedopens a custom view'sbodyto 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:
#Keylinelives in your previews; your views render exactly as before everywhere else.
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 |
| 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 |
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.
Three steps:
import KeylineKit#Preview {
#Keyline {
VStack(spacing: 16) {
ProfileView()
}
.padding(24)
}
}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()reads16 · default, andVStack(spacing: Theme.spacing)reads12 · Theme.spacing.
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() }Layer outlines are on by default. Turn them off with showContainerLines: false.
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.
#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 aslabel:,header:andfooter:. - The branches of
if,switchand#ifare entered. ForEachandGroupwithout 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,MapandTable.Button(action:) { … }andButton { … } label: { … }still instrument their label. - Literals that Xcode rewrites for live editing (
__designTimeInteger(…)) are shown with their value.
⚠️ Current limitations
.cornersreads the radius from modifiers, not from a root shape such asRoundedRectangle(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.
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"]
- 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. - Measurement (
Sources/KeylineKit/Measurement): each node and probe reports its global frame withonGeometryChange. This also works insideNavigationStackandScrollView. - Drawing (
Sources/KeylineKit/Rendering): a pure, tested layout step turns the frames into regions, dimension lines and non-overlapping labels, and a singleCanvasdraws them.
Types prefixed with _ are public only because the expanded code uses them;
they are hidden from the documentation.
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 safeAreaModes: defaults, safeArea and details.
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/KeylineKitMacrosTestsand layout logic inTests/KeylineKitTests.
Run the tests with:
swift test📣 If
xcode-selectpoints 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.
Made by Helio Martín.
KeylineKit is available under the MIT license.

