├── 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 | --------------------------------------------------------------------------------