Skip to content

Commit 3da505c

Browse files
committed
Initial commit
0 parents  commit 3da505c

14 files changed

+1080
-0
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Robert Böhnke
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "Redline",
8+
platforms: [
9+
.iOS(.v18),
10+
.macOS(.v15),
11+
],
12+
products: [
13+
// Products define the executables and libraries a package produces, making them visible to other packages.
14+
.library(
15+
name: "Redline",
16+
targets: ["Redline"]),
17+
],
18+
targets: [
19+
// Targets are the basic building blocks of a package, defining a module or a test suite.
20+
// Targets can depend on other targets in this package and products from dependencies.
21+
.target(
22+
name: "Redline"),
23+
]
24+
)

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Redline
2+
3+
## Easy Redlines for SwiftUI
4+
5+
With Redline, you can quickly visualize positions, sizes, spacings and alignment guides to verify your implementation against specs or to debug layout problem.
6+
7+
![](/example.png)
8+
9+
```swift
10+
import Redline
11+
12+
GroupBox {
13+
VStack(spacing: 24) {
14+
Image(systemName: "globe")
15+
.resizable()
16+
.aspectRatio(contentMode: .fit)
17+
.frame(width: 80, height: 80)
18+
.foregroundStyle(.tint)
19+
.measureSpacing()
20+
.visualizePosition(color: .blue, in: .named("outside"))
21+
.visualizeSize()
22+
23+
HStack(alignment: .firstTextBaseline) {
24+
Image(systemName: "figure.wave")
25+
.visualizeAlignmentGuide(.firstTextBaseline)
26+
27+
Text("Hello, world!\nHow are you?")
28+
.visualizeAlignmentGuide(.firstTextBaseline)
29+
}
30+
.measureSpacing()
31+
32+
Text("Thank you, bye").font(.caption)
33+
.measureSpacing()
34+
.visualizePosition(color: .blue, edges: [.bottom, .trailing], in: .named("outside"))
35+
}
36+
.visualizeSpacing(axis: .vertical)
37+
.padding(8)
38+
}
39+
.visualizeSize()
40+
.coordinateSpace(name: "outside")
41+
.visualizePosition(color: .blue)
42+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import CoreGraphics
2+
3+
extension CGPoint {
4+
func offset(x: CGFloat = 0, y: CGFloat = 0) -> CGPoint {
5+
var copy = self
6+
copy.x += x
7+
copy.y += y
8+
9+
return copy
10+
}
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import SwiftUI
2+
3+
extension CGRect {
4+
subscript(unitPoint: UnitPoint) -> CGPoint {
5+
.init(x: minX + width * unitPoint.x, y: minY + height * unitPoint.y)
6+
}
7+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import SwiftUI
2+
3+
struct DimensionLabel<Content: View>: View {
4+
@ViewBuilder var content: Content
5+
6+
var edge: Edge?
7+
8+
var body: some View {
9+
content
10+
.textRenderer(SubpixelRenderer())
11+
.contentTransition(.identity)
12+
.transaction { t in
13+
t.disablesAnimations = true
14+
t.animation = nil
15+
}
16+
.font(font)
17+
.kerning(0.2)
18+
.multilineTextAlignment(.center)
19+
.foregroundStyle(.white.shadow(.drop(color: .black.opacity(0.4), radius: 0, x: 1, y: 1)))
20+
.padding(.horizontal, 2.5)
21+
.padding(.vertical, 1.3)
22+
.background(.foreground, in: LabelBackground(edge: edge))
23+
.fixedSize()
24+
.alignmentGuide(HorizontalAlignment.dimensionLabel) { d in
25+
switch edge {
26+
case .top, .bottom, nil: d[HorizontalAlignment.center]
27+
case .leading: d[.leading] - 8
28+
case .trailing: d[.trailing] + 8
29+
}
30+
}
31+
.alignmentGuide(VerticalAlignment.dimensionLabel) { d in
32+
switch edge {
33+
case .leading, .trailing, nil: d[VerticalAlignment.center]
34+
case .top: d[.top] - 8
35+
case .bottom: d[.bottom] + 8
36+
}
37+
}
38+
.geometryGroup()
39+
}
40+
41+
var font: Font {
42+
Font.system(size: 7.25, weight: .semibold)
43+
.monospacedDigit()
44+
.width(.init(-0.1))
45+
}
46+
47+
init(edge: Edge? = nil, value: CGFloat) where Content == Text {
48+
self.content = Text(value, format: .number.precision(.fractionLength(2 ... 2)))
49+
self.edge = edge
50+
}
51+
52+
init(edge: Edge? = nil, value: CGSize) where Content == Text {
53+
self.content = Text("\(value.width, format: .number.precision(.fractionLength(2 ... 2)))×\(value.height, format: .number.precision(.fractionLength(2 ... 2)))")
54+
self.edge = edge
55+
}
56+
}
57+
58+
extension VerticalAlignment {
59+
struct DimensionLabel: AlignmentID {
60+
static func defaultValue(in context: ViewDimensions) -> CGFloat {
61+
context[VerticalAlignment.center]
62+
}
63+
}
64+
65+
static let dimensionLabel = VerticalAlignment(DimensionLabel.self)
66+
}
67+
68+
extension HorizontalAlignment {
69+
struct DimensionLabel: AlignmentID {
70+
static func defaultValue(in context: ViewDimensions) -> CGFloat {
71+
context[HorizontalAlignment.center]
72+
}
73+
}
74+
75+
static let dimensionLabel = HorizontalAlignment(DimensionLabel.self)
76+
}
77+
78+
extension Alignment {
79+
static var dimensionLabel: Alignment {
80+
.init(horizontal: .dimensionLabel, vertical: .dimensionLabel)
81+
}
82+
}
83+
84+
private struct LabelBackground: Shape {
85+
var edge: Edge?
86+
87+
var cornerRadius: CGFloat = 3
88+
89+
nonisolated func path(in rect: CGRect) -> Path {
90+
var triangle = Path()
91+
92+
let r = cornerRadius * 2 * 1.528665
93+
94+
let dimension = edge == .leading || edge == .trailing ? rect.height : rect.width
95+
let w = max(2, min((dimension - r) / 2, 4))
96+
97+
switch edge {
98+
case nil:
99+
break
100+
case .leading:
101+
triangle.addLines([
102+
rect[.leading].offset(y: w),
103+
rect[.leading].offset(x: -4),
104+
rect[.leading].offset(y: -w)
105+
])
106+
case .trailing:
107+
triangle.addLines([
108+
rect[.trailing].offset(y: w),
109+
rect[.trailing].offset(x: 4),
110+
rect[.trailing].offset(y: -w)
111+
])
112+
case .top:
113+
triangle.addLines([
114+
rect[.top].offset(x: w),
115+
rect[.top].offset(y: -4),
116+
rect[.top].offset(x: -w)
117+
])
118+
case .bottom:
119+
triangle.addLines([
120+
rect[.bottom].offset(x: w),
121+
rect[.bottom].offset(y: 4),
122+
rect[.bottom].offset(x: -w)
123+
])
124+
}
125+
triangle.closeSubpath()
126+
127+
return Path(roundedRect: rect, cornerRadius: cornerRadius).union(triangle)
128+
}
129+
}
130+
131+
struct SubpixelRenderer: TextRenderer {
132+
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
133+
ctx.translateBy(x: 0, y: -0.33333333)
134+
135+
for line in layout {
136+
ctx.draw(line, options: .disablesSubpixelQuantization)
137+
}
138+
}
139+
}
140+
141+
#Preview {
142+
VStack {
143+
DimensionLabel(edge: .leading, value: 1234567890)
144+
DimensionLabel(edge: .top, value: 1234567890)
145+
DimensionLabel(edge: .trailing, value: 1234567890)
146+
DimensionLabel(edge: .bottom, value: 1234567890)
147+
148+
DimensionLabel(value: CGSize(width: 100, height: 100))
149+
}
150+
151+
VStack {
152+
DimensionLabel(value: 10.20)
153+
DimensionLabel(value: 10.666667)
154+
DimensionLabel(value: 10.3333333333)
155+
DimensionLabel(value: 10.99999)
156+
}
157+
.foregroundStyle(.purple)
158+
}

0 commit comments

Comments
 (0)