├── Assets
├── Toggle.png
└── DatePicker.png
├── .gitignore
├── Sources
└── GeometryWriter
│ ├── GeometryWriterProxy.swift
│ ├── GeometryWriter.swift
│ └── GeometryWriterViewModel.swift
├── Package.swift
├── README.md
└── Package.resolved
/Assets/Toggle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heestand-xyz/GeometryWriter/HEAD/Assets/Toggle.png
--------------------------------------------------------------------------------
/Assets/DatePicker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/heestand-xyz/GeometryWriter/HEAD/Assets/DatePicker.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/Sources/GeometryWriter/GeometryWriterProxy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Anton Heestand on 2022-03-16.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public struct GeometryWriterProxy {
11 |
12 | let origin: CGPoint
13 | public let size: CGSize
14 |
15 | public enum CoordinateSpace {
16 | case local
17 | case global
18 | }
19 |
20 | public func frame(in coordinateSpace: CoordinateSpace) -> CGRect {
21 | switch coordinateSpace {
22 | case .local:
23 | return CGRect(origin: .zero, size: size)
24 | case .global:
25 | return CGRect(origin: origin, size: size)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.4
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "GeometryWriter",
7 | platforms: [
8 | .iOS(.v14),
9 | ],
10 | products: [
11 | .library(
12 | name: "GeometryWriter",
13 | targets: ["GeometryWriter"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/heestand-xyz/PixelKit", .exactItem("3.0.3")),
17 | .package(url: "https://github.com/heestand-xyz/MultiViews", .exactItem("1.5.6")),
18 | .package(url: "https://github.com/heestand-xyz/CoreGraphicsExtensions", .exactItem("1.2.1")),
19 | ],
20 | targets: [
21 | .target(
22 | name: "GeometryWriter",
23 | dependencies: ["PixelKit", "MultiViews", "CoreGraphicsExtensions"]),
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI GeometryWriter
2 |
3 | The `GeometryWriter` writes the minimum frame to a `View`.
4 |
5 | It is the opposite of a `GeometryReader`, as in that the `GeometryReader` will take up as much space as possible and the `GeometryWriter` will take up as little space as possible, constraining a view to it's visible frame. Any area with opacity `0.0` will be cropped.
6 |
7 |
8 |
9 |
10 |
11 | The blue border is the resulting frame of a view in a `GeometryWriter`.
12 |
13 | ## Code Example
14 |
15 | ```swift
16 | import SwiftUI
17 | import GeometryWriter
18 |
19 | struct ContentView: View {
20 |
21 | @State private var active: Bool = false
22 |
23 | var body: some View {
24 |
25 | GeometryWriter { _ in
26 |
27 | Toggle(isOn: $active) {
28 | EmptyView()
29 | }
30 | }
31 | .border(.blue)
32 | }
33 | }
34 | ```
35 |
36 | ## Swift Package
37 |
38 | ```swift
39 | .package(url: "https://github.com/heestand-xyz/GeometryWriter", from: "1.0.0")
40 | ```
41 |
42 | Minimum requirement: iOS 14
43 |
--------------------------------------------------------------------------------
/Sources/GeometryWriter/GeometryWriter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryWriter.swift
3 | //
4 | //
5 | // Created by Anton Heestand on 2022-03-15.
6 | //
7 |
8 | import SwiftUI
9 | import CoreGraphicsExtensions
10 |
11 | public struct GeometryWriter: View {
12 |
13 | private let content: (GeometryWriterProxy) -> Content
14 |
15 | @StateObject private var viewModel: GeometryWriterViewModel
16 |
17 | public init(content: @escaping (GeometryWriterProxy) -> Content) {
18 | self.content = content
19 | _viewModel = StateObject(wrappedValue: GeometryWriterViewModel(content: {
20 | let emptyProxy = GeometryWriterProxy(origin: .zero, size: .zero)
21 | return content(emptyProxy)
22 | }))
23 | }
24 |
25 | public var body: some View {
26 | if let frame = viewModel.frame {
27 | GeometryReader { geometryProxy in
28 | let origin = geometryProxy.frame(in: .global).origin + frame.origin
29 | let proxy = GeometryWriterProxy(origin: origin, size: frame.size)
30 | content(proxy)
31 | .frame(width: 0, height: 0)
32 | .frame(width: viewModel.maximumSize.width,
33 | height: viewModel.maximumSize.height)
34 | }
35 | .frame(width: frame.width, height: frame.height, alignment: .topLeading)
36 | .offset(x: -frame.origin.x, y: -frame.origin.y)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CoreGraphicsExtensions",
6 | "repositoryURL": "https://github.com/heestand-xyz/CoreGraphicsExtensions",
7 | "state": {
8 | "branch": null,
9 | "revision": "a40dc230306daacb410f9a2f36f63a811ea2e88e",
10 | "version": "1.2.1"
11 | }
12 | },
13 | {
14 | "package": "MultiViews",
15 | "repositoryURL": "https://github.com/heestand-xyz/MultiViews",
16 | "state": {
17 | "branch": null,
18 | "revision": "cbd14515dc251065a02955dbaca781cb224fe613",
19 | "version": "1.5.6"
20 | }
21 | },
22 | {
23 | "package": "PixelColor",
24 | "repositoryURL": "https://github.com/heestand-xyz/PixelColor",
25 | "state": {
26 | "branch": null,
27 | "revision": "23851e7ea5f2dc844dd6d9a4d730864199583921",
28 | "version": "1.3.2"
29 | }
30 | },
31 | {
32 | "package": "PixelKit",
33 | "repositoryURL": "https://github.com/heestand-xyz/PixelKit",
34 | "state": {
35 | "branch": null,
36 | "revision": "f663ac4951ac04138797dfc5d819c875b7010325",
37 | "version": "3.0.3"
38 | }
39 | },
40 | {
41 | "package": "RenderKit",
42 | "repositoryURL": "https://github.com/heestand-xyz/RenderKit",
43 | "state": {
44 | "branch": null,
45 | "revision": "55a8fa77c6fc1bb7eb70ebad486d28748093b489",
46 | "version": "2.0.1"
47 | }
48 | },
49 | {
50 | "package": "Resolution",
51 | "repositoryURL": "https://github.com/heestand-xyz/Resolution",
52 | "state": {
53 | "branch": null,
54 | "revision": "0fdee03d7b312f075897cdac437f366cc6631d6a",
55 | "version": "1.0.4"
56 | }
57 | },
58 | {
59 | "package": "TextureMap",
60 | "repositoryURL": "https://github.com/heestand-xyz/TextureMap",
61 | "state": {
62 | "branch": null,
63 | "revision": "92a11461c9125951ef17eb578871853cca9920a7",
64 | "version": "0.1.1"
65 | }
66 | }
67 | ]
68 | },
69 | "version": 1
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/GeometryWriter/GeometryWriterViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryWriterViewModel.swift
3 | //
4 | //
5 | // Created by Anton Heestand on 2022-03-15.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import PixelKit
11 | import RenderKit
12 | import MultiViews
13 |
14 | final class GeometryWriterViewModel: ObservableObject {
15 |
16 | private let viewPix: ViewPIX
17 | private let xReducePix: ReducePIX
18 | private let yReducePix: ReducePIX
19 |
20 | private let hostingController: MPHostingController
21 | private var view: MPView { hostingController.view }
22 |
23 | let maximumSize = CGSize(width: 500, height: 500)
24 |
25 | private var containerView: MPView
26 |
27 | private var topFraction: CGFloat?
28 | private var leadingFraction: CGFloat?
29 | private var trailingFraction: CGFloat?
30 | private var bottomFraction: CGFloat?
31 |
32 | @Published var fractionFrame: CGRect?
33 |
34 | var origin: CGPoint? {
35 | guard let frame = fractionFrame else { return nil }
36 | return CGPoint(x: frame.minX * maximumSize.width,
37 | y: frame.minY * maximumSize.height)
38 | }
39 |
40 | var size: CGSize? {
41 | guard let frame = fractionFrame else { return nil }
42 | return CGSize(width: frame.width * maximumSize.width,
43 | height: frame.height * maximumSize.height)
44 | }
45 |
46 | var frame: CGRect? {
47 | guard let origin = origin else { return nil }
48 | guard let size = size else { return nil }
49 | return CGRect(origin: origin, size: size)
50 | }
51 |
52 | init(content: () -> Content) {
53 |
54 | PixelKit.main.disableLogging()
55 |
56 | viewPix = ViewPIX()
57 | viewPix.autoSize = false
58 |
59 | xReducePix = ReducePIX()
60 | xReducePix.input = viewPix
61 | xReducePix.cellList = .row
62 | xReducePix.method = .maximum
63 |
64 | yReducePix = ReducePIX()
65 | yReducePix.input = viewPix
66 | yReducePix.cellList = .column
67 | yReducePix.method = .maximum
68 |
69 | hostingController = MPHostingController(rootView: content())
70 |
71 | containerView = MPView()
72 | containerView.frame = CGRect(origin: .zero, size: maximumSize)
73 | containerView.addSubview(view)
74 |
75 | #if os(macOS)
76 | view.wantsLayer = true
77 | view.layer!.backgroundColor = .clear
78 | #else
79 | view.backgroundColor = .clear
80 | #endif
81 |
82 | view.translatesAutoresizingMaskIntoConstraints = false
83 | NSLayoutConstraint.activate([
84 | view.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
85 | view.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
86 | ])
87 |
88 | viewPix.renderView = containerView
89 |
90 | yReducePix.delegate = self
91 | xReducePix.delegate = self
92 |
93 | forceRender()
94 | }
95 |
96 | func forceRender() {
97 |
98 | DispatchQueue.main.async { [weak self] in
99 | self?.render()
100 | }
101 | }
102 |
103 | func render() {
104 | viewPix.viewNeedsRender()
105 | }
106 | }
107 |
108 | extension GeometryWriterViewModel: NODEDelegate {
109 |
110 | func nodeDidRender(_ node: NODE) {
111 |
112 | guard let pix = node as? PIX else { return }
113 |
114 | guard pix == xReducePix || pix == yReducePix else { return }
115 |
116 | guard let pixels = pix.renderedPixels else { return }
117 |
118 | let values = pixels.raw.flatMap({ $0 }).map(\.color.alpha)
119 |
120 | if pix == xReducePix {
121 |
122 | var leadingIndex: Int!
123 | for (index, value) in values.enumerated() {
124 | if value > 0.01 {
125 | leadingIndex = index
126 | break
127 | }
128 | }
129 |
130 | var trailingIndex: Int!
131 | for (index, value) in values.enumerated().reversed() {
132 | if value > 0.01 {
133 | trailingIndex = index
134 | break
135 | }
136 | }
137 |
138 | guard leadingIndex != nil && trailingIndex != nil else { return }
139 |
140 | leadingFraction = CGFloat(leadingIndex) / CGFloat(values.count)
141 | trailingFraction = CGFloat(trailingIndex + 1) / CGFloat(values.count)
142 |
143 | } else if pix == yReducePix {
144 |
145 | var topIndex: Int!
146 | for (index, value) in values.enumerated() {
147 | if value > 0.01 {
148 | topIndex = index
149 | break
150 | }
151 | }
152 |
153 | var bottomIndex: Int!
154 | for (index, value) in values.enumerated().reversed() {
155 | if value > 0.01 {
156 | bottomIndex = index
157 | break
158 | }
159 | }
160 |
161 | guard topIndex != nil && bottomIndex != nil else { return }
162 |
163 | topFraction = CGFloat(topIndex) / CGFloat(values.count)
164 | bottomFraction = CGFloat(bottomIndex + 1) / CGFloat(values.count)
165 | }
166 |
167 | calculateFractionFrame()
168 | }
169 |
170 | func calculateFractionFrame() {
171 |
172 | guard let topFraction: CGFloat = topFraction else { return }
173 | guard let leadingFraction: CGFloat = leadingFraction else { return }
174 | guard let trailingFraction: CGFloat = trailingFraction else { return }
175 | guard let bottomFraction: CGFloat = bottomFraction else { return }
176 |
177 | let width: CGFloat = trailingFraction - leadingFraction
178 | let height: CGFloat = bottomFraction - topFraction
179 |
180 | fractionFrame = CGRect(x: leadingFraction, y: topFraction, width: width, height: height)
181 | }
182 | }
183 |
--------------------------------------------------------------------------------