Skip to content

Commit c137252

Browse files
committed
Add SwiftUI modifiers, basic geometry tests
1 parent 964f7cd commit c137252

File tree

4 files changed

+150
-8
lines changed

4 files changed

+150
-8
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Geometry
2-
A repository of common geometric algorithms and `CoreGraphics` type extensions.
2+
A repository of common geometric algorithms, `SwiftUI` and `CoreGraphics` type extensions.
33

44
## Algorithms
55
At present, the package contains no algorithms.
66

7+
## `SwiftUI` Extensions
8+
Geometry defines a `FrameReader` extension used to more easily gather geometry
9+
from a SwiftUI view hierarchy.
10+
711
## `CoreGraphics` Extensions
812
Geometry defines `CGVectorType` a protocol unifying the various 2D CoreGraphics
913
types including `CGPoint`, `CGSize`, and `CGVector`. The type provides support for
@@ -12,4 +16,12 @@ easy conversion among types.
1216
The type also allows for a concise definition of standard operators for addition,
1317
subtraction, multiplication, and division involving `CGVectorType` types.
1418

15-
Finally, the package includes several extensions to `CGRect`.
19+
Finally, the package includes several extensions to `CGRect`:
20+
21+
```swift
22+
// fetch point at min X, mid Y
23+
let midLeft = rect.at(.min, .mid)
24+
25+
// fetch the midpoint
26+
let bottomRight = rect.at(.max, .max)
27+
```

Sources/Geometry/CoreGraphics.swift renamed to Sources/Geometry/Extensions/CoreGraphics.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ extension CGVector: CGVectorType {
144144
}
145145

146146
extension CGRect {
147+
/// Initializes a `CGRect` from one or more points
148+
/// - Parameter points: a list of one or more points
149+
public init(_ points: CGVectorType...) {
150+
let xs = points.map(\.dx)
151+
let ys = points.map(\.dy)
152+
153+
let xmin = xs.min() ?? xs[0]
154+
let xmax = xs.max() ?? xs[0]
155+
let ymin = ys.min() ?? ys[0]
156+
let ymax = ys.max() ?? ys[0]
157+
158+
self.init(x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin)
159+
}
160+
147161
public struct Position: Hashable, Codable {
148162
let value: CGFloat
149163
private init(_ value: CGFloat) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// FrameReader.swift
3+
// Geometry
4+
//
5+
// Created by Mark Onyschuk on 12/04/23.
6+
// Copyright © 2023 Dimension North Inc. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
private struct FrameReader: ViewModifier {
12+
struct Frame: PreferenceKey {
13+
static var defaultValue: CGRect = .zero
14+
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
15+
}
16+
17+
var onChange: (CGRect) -> ()
18+
var coordinates: CoordinateSpace
19+
20+
init(coordinates: CoordinateSpace, onChange: @escaping (CGRect) -> ()) {
21+
self.onChange = onChange
22+
self.coordinates = coordinates
23+
}
24+
25+
public func body(content: Content) -> some View {
26+
content.overlay(GeometryReader {
27+
geom in
28+
Color.clear.preference(
29+
key: Frame.self,
30+
value: geom.frame(in: coordinates)
31+
)
32+
})
33+
.onPreferenceChange(Frame.self, perform: onChange)
34+
}
35+
}
36+
37+
extension View {
38+
39+
/// An extension whose `value` tracks the enclosed view's bounding rectangle in `coordinates`
40+
/// - Parameters:
41+
/// - value: a `CGRect` binding
42+
/// - coordinates: a coordinate space
43+
/// - Returns: a modified view
44+
public func rect(_ value: Binding<CGRect>, in coordinates: CoordinateSpace = .global) -> some View {
45+
self.modifier(FrameReader(coordinates: coordinates) {
46+
value.wrappedValue = $0
47+
})
48+
}
49+
50+
/// An extension whose `value` tracks the enclosed view's size in `coordinates`
51+
/// - Parameters:
52+
/// - value: a `CGSize` binding
53+
/// - coordinates: a coordinate space
54+
/// - Returns: a modified view
55+
public func size(_ value: Binding<CGSize>, in coordinates: CoordinateSpace = .global) -> some View {
56+
self.modifier(FrameReader(coordinates: coordinates) {
57+
value.wrappedValue = $0.size
58+
})
59+
}
60+
61+
/// An extension whose `value` tracks the enclosed view's origin in `coordinates`
62+
/// - Parameters:
63+
/// - value: a `CGPoint` binding
64+
/// - coordinates: a coordinate space
65+
/// - Returns: a modified view
66+
public func origin(_ value: Binding<CGPoint>, in coordinates: CoordinateSpace = .global) -> some View {
67+
self.modifier(FrameReader(coordinates: coordinates) {
68+
value.wrappedValue = $0.origin
69+
})
70+
}
71+
}
72+
Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,56 @@
11
import XCTest
22
@testable import Geometry
33

4-
final class GeometryTests: XCTestCase {
5-
func testExample() throws {
6-
// XCTest Documentation
7-
// https://developer.apple.com/documentation/xctest
4+
final class CGRectTests: XCTestCase {
5+
func testItReturnsLocationsWithinIt() {
6+
let r1 = CGRect(origin: .init(0, 0), size: .init(100, 100))
7+
8+
XCTAssert(r1.at(.min, .min) == CGPoint(0, 0))
9+
XCTAssert(r1.at(.max, .max) == CGPoint(100, 100))
10+
XCTAssert(r1.at(.mid, .max) == CGPoint(50, 100))
11+
}
12+
13+
func testItCanBeInitializedFromPoints() {
14+
let p0 = CGPoint(0, 0)
15+
let p1 = CGPoint(50, 50)
16+
let p2 = CGPoint(100, 100)
17+
18+
let r1 = CGRect(p0)
19+
20+
XCTAssert(r1.origin == p0)
21+
XCTAssert(r1.size == .zero)
22+
23+
let r2 = CGRect(p0, p1)
24+
25+
XCTAssert(r2.origin == p0)
26+
XCTAssert(r2.size == CGSize(p1 - p0))
27+
28+
let r3 = CGRect(p0, p1, p2)
29+
30+
XCTAssert(r3.origin == p0)
31+
XCTAssert(r3.size == CGSize(p2 - p0))
32+
33+
XCTAssert(r3.contains(p1))
34+
}
35+
}
836

9-
// Defining Test Cases and Test Methods
10-
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
37+
final class CGVectorTypeTests: XCTestCase {
38+
func testVectorTypesCanBeConverted() {
39+
let p1 = CGPoint(1, 2)
40+
let s2 = CGSize(p1)
41+
42+
XCTAssert(s2.width == 1 && s2.height == 2)
43+
}
44+
45+
func testVectorTypesOfferCommonOperators() {
46+
let s1 = CGSize(100, 100)
47+
48+
XCTAssert(s1 * 2 == CGSize(200, 200))
49+
XCTAssert(s1 + (0, 5) == CGSize(100, 105))
50+
XCTAssert(s1 * (1, 0.5) == CGSize(100, 50))
51+
52+
XCTAssert(s1 / 2 == CGSize(50, 50))
53+
XCTAssert(s1 - (0, 5) == CGSize(100, 95))
54+
XCTAssert(s1 / (1, 0.5) == CGSize(100, 100))
1155
}
1256
}

0 commit comments

Comments
 (0)