Skip to content

Instantly share code, notes, and snippets.

@ole
Last active December 16, 2022 06:47
Show Gist options
  • Save ole/d02deec283b0f32440af86df93a9ffaa to your computer and use it in GitHub Desktop.
Save ole/d02deec283b0f32440af86df93a9ffaa to your computer and use it in GitHub Desktop.
SwiftUI: Show a view with its ideal size, but no larger than a given maximum size. The view must be flexible, otherwise it may stretch out of the wrapping frame. Two variants: as a custom Layout (iOS 16+), or with manual measuring using GeometryReader.
import SwiftUI
extension View {
func noLargerThan(_ maxSize: CGSize) -> some View {
NoLargerThanLayout(maxSize: maxSize) {
self
}
}
}
struct NoLargerThanLayout: Layout {
var maxSize: CGSize
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
assert(subviews.count == 1)
let idealSize = subviews[0].sizeThatFits(.unspecified)
return Self.targetSize(idealSize: idealSize, maxSize: maxSize)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
assert(subviews.count == 1)
let idealSize = subviews[0].sizeThatFits(.unspecified)
let targetSize = Self.targetSize(idealSize: idealSize, maxSize: maxSize)
subviews[0].place(
at: CGPoint(x: bounds.midX, y: bounds.midY),
anchor: .center,
proposal: ProposedViewSize(targetSize)
)
}
private static func targetSize(idealSize: CGSize, maxSize: CGSize) -> CGSize {
// Use idealSize's aspect ratio, fit into maxSize.
let idealAspectRatio = idealSize.width / idealSize.height
let maxAspectRatio = maxSize.width / maxSize.height
if idealAspectRatio >= maxAspectRatio {
let width = min(idealSize.width, maxSize.width)
return CGSize(width: width, height: width / idealAspectRatio)
} else {
let height = min(idealSize.height, maxSize.height)
return CGSize(width: height * idealAspectRatio, height: height)
}
}
}
struct ContentView: View {
var body: some View {
let maxSize = CGSize(width: 300, height: 300)
ScrollView {
Image("david-marcu-VfUN94cUy4o-unsplash-200px")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash-rotated-200px")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash-rotated")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
extension View {
func noLargerThan(_ maxSize: CGSize) -> some View {
modifier(NoLargerThanModifier(maxSize: maxSize))
}
}
struct NoLargerThanModifier: ViewModifier {
var maxSize: CGSize
@State private var idealSize: CGSize? = nil
func body(content: Content) -> some View {
content
.frame(width: targetSize.width, height: targetSize.height)
.background {
// Measure size of content
content
.hidden()
.fixedSize()
.overlay {
GeometryReader { proxy in
Color.clear
.onAppear {
idealSize = proxy.size
}
.onChange(of: proxy.size) { _ in
idealSize = proxy.size
}
}
}
}
}
private var targetSize: CGSize {
guard let idealSize else {
return maxSize
}
// Use idealSize's aspect ratio, fit into maxSize.
let idealAspectRatio = idealSize.width / idealSize.height
let maxAspectRatio = maxSize.width / maxSize.height
if idealAspectRatio >= maxAspectRatio {
let width = min(idealSize.width, maxSize.width)
return CGSize(width: width, height: width / idealAspectRatio)
} else {
let height = min(idealSize.height, maxSize.height)
return CGSize(width: height * idealAspectRatio, height: height)
}
}
}
struct ContentView: View {
var body: some View {
let maxSize = CGSize(width: 300, height: 300)
ScrollView {
Image("david-marcu-VfUN94cUy4o-unsplash-200px")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash-rotated-200px")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
Image("david-marcu-VfUN94cUy4o-unsplash-rotated")
.resizable()
.aspectRatio(contentMode: .fit)
.noLargerThan(maxSize)
.border(.red)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment