Last active
October 27, 2024 07:11
-
-
Save swiftui-lab/283d72e55124d9c7148113710bad46c4 to your computer and use it in GitHub Desktop.
A debugging modifier for SwiftUI views (showSizes)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Author: SwiftUI-Lab (swiftui-lab.com) | |
// Description: Implementation of the showSizes() debugging modifier | |
// blog article: https://swiftui-lab.com/layout-protocol-part-2 | |
import SwiftUI | |
struct MeasureExample: View { | |
var body: some View { | |
VStack { | |
HStack { | |
ScrollView { | |
Text("Hello world!") | |
} | |
.showSizes([.current, .maximum]) | |
Rectangle() | |
.fill(.yellow) | |
.showSizes() | |
Text("Hello world") | |
.showSizes() | |
Image("clouds") | |
.showSizes() | |
Image("clouds") | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.showSizes([.minimum, .ideal, .maximum, .current]) | |
} | |
.padding(60) | |
} | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
.background(.white) | |
} | |
} | |
extension View { | |
// If proposal is nil, get min, ideal and max sizes | |
@ViewBuilder func showSizes(_ proposals: [MeasureLayout.SizeRequest] = [.minimum, .ideal, .maximum]) -> some View { | |
Measure(proposals: proposals) { self } | |
} | |
} | |
struct Measure<V: View>: View { | |
@State private var reportedSizes: [CGSize] = [] | |
let proposals: [MeasureLayout.SizeRequest] | |
@ViewBuilder let content: () -> V | |
var body: some View { | |
MeasureLayout { | |
content() | |
.layoutValue(key: MeasureLayout.InfoRequest.self, value: proposals) | |
.layoutValue(key: MeasureLayout.InfoReply.self, value: $reportedSizes) | |
.overlay(alignment: .topTrailing) { | |
Text(mergedSizes) | |
.background(.gray) | |
.foregroundColor(.white) | |
.font(.caption) | |
.offset(y: -20) | |
.fixedSize() | |
} | |
} | |
} | |
var mergedSizes: String { | |
String(reportedSizes.map { String(format: "(%.1f, %.1f)" , $0.width, $0.height) }.joined(separator: " - ")) | |
} | |
} | |
struct MeasureLayout: Layout { | |
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { | |
return subviews[0].sizeThatFits(proposal) | |
} | |
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { | |
DispatchQueue.main.async { | |
subviews[0][InfoReply.self]?.wrappedValue = subviews[0][InfoRequest.self].map { | |
$0.size(view: subviews[0], proposal: proposal) | |
} | |
} | |
subviews[0].place(at: CGPoint(x: bounds.midX, y: bounds.midY), anchor: .center, proposal: proposal) | |
} | |
struct InfoRequest: LayoutValueKey { | |
static var defaultValue: [SizeRequest] = [] | |
} | |
struct InfoReply: LayoutValueKey { | |
static var defaultValue: Binding<[CGSize]>? = nil | |
} | |
enum SizeRequest { | |
case minimum | |
case ideal | |
case maximum | |
case current | |
case proposal(size: ProposedViewSize) | |
func size(view: LayoutSubview, proposal: ProposedViewSize) -> CGSize { | |
switch self { | |
case .minimum: return view.sizeThatFits(.zero) | |
case .ideal: return view.sizeThatFits(.unspecified) | |
case .maximum: return view.sizeThatFits(.infinity) | |
case .current: return view.sizeThatFits(proposal) | |
case .proposal(let prop): return view.sizeThatFits(prop) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very nice – but be aware
String format %.1f
uses rounding. So the decimal you see may be rounded and incorrect!