Last active
December 16, 2022 06:47
-
-
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.
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
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() | |
} | |
} |
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
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