Reputation: 7729
How can a View
adjust its ideal size in one dimension when the other dimension is clamped?
For instance, Text
exhibits this behavior. If constrained via .fixedSize
and given too little space in one dimension, then the ideal size for the other dimension will grow in response, if possible.
Using .fixedSize(horizontal: false, vertical: false)
Using .fixedSize(horizontal: false, vertical: true)
Using .fixedSize(horizontal: true, vertical: false)
It looks like Text
has somehow defined its ideal size as something like .frame(idealWidth: 400, idealHeight: 20)
in the first two cases. However, the part that's confusing me is that it seems to be modifying that in the third case to something like .frame(idealWidth: 80, idealHeight: 100)
.
How can we, like Text
, allow our View
to update its ideal size in response to a dimension(s) being clamped?
Here's a bit of code that shows this behavior in Text
which I described above:
VStack(spacing: 100) {
Group {
Text("ooooooooooooooooooooooooooooooooooo")
.fixedSize(horizontal: false, vertical: false)
Text("ooooooooooooooooooooooooooooooooooo")
.fixedSize(horizontal: true, vertical: false)
Text("ooooooooooooooooooooooooooooooooooo")
.fixedSize(horizontal: false, vertical: true)
}
.background(Color.green)
.frame(width: 75, height: 75)
.background(Color.blue)
}
With this font and repeating "o" content, Text
wants to basically have square space equal to about 7,000 points (338x20 ideal size for case #2 and 68x108 for case #3).
How could this be replicated in a custom View
? The minHeight
and minWidth
are not static; They depend on the parent having clamped a dimension or not. How can we, as the child, know which dimensions we've been clamped in, if any? The proposed size from the parent is still 75x75 in each example, so what is it that the child view has access to with which to base its ideal size off of? How is Text
doing this?
To make an answer more concrete, consider how you could replace the Text("oooooo")
instances with instances of a custom class which behaves similarly with regard to .fixedSize
. ie. attempt to maintain a 7,000 point square area, with a minimum 20 point height, preferring to grow horizontally first.
Upvotes: 1
Views: 3576
Reputation: 21219
It's problematic due to the nature how the fixedSize
works:
min
, ideal
and max
values for the width
& height
where min <= ideal <= max
.ideal
values will differ for horizontal/vertical fix.The commented code is self explanatory.
import SwiftUI
extension View {
func frame(size: CGSize) -> some View {
self.frame(width: size.width, height: size.height)
}
}
struct TextLike: View {
// It's like text, how much space we'd like to occupy
var squareArea: CGFloat
// Text() draws at least one line of text, we'll do the same
private let preferredHeight: CGFloat = 20
// Preferred/Max width in case of one line of text
private let preferredWidth: CGFloat
init(squareArea: CGFloat) {
self.squareArea = squareArea
self.preferredWidth = squareArea / preferredHeight
}
var body: some View {
GeometryReader { proxy in
// Let's say that this is the text
Color.green.opacity(0.5)
.frame(size: self.size(withProxy: proxy))
}
.frame(
// Ideally we'd like to draw it as one line of text
idealWidth: preferredWidth,
// Maximum width equals to ideal width (one line of text)
maxWidth: preferredWidth,
// At least one line of text
minHeight: preferredHeight,
// Ideally we'd like to draw it as one line of text
idealHeight: preferredHeight
)
}
private func size(withProxy proxy: GeometryProxy) -> CGSize {
//
// Good enough for the demonstration, but you should get PhD & read:
//
// https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
// https://stackoverflow.com/a/10335601/581190
// https://stackoverflow.com/a/10334868/581190
//
// Also it must be generic enough to cover different devices, ...
//
// proxy.size.width on iPhone SE (2nd) = 350.0
// proxy.size.width on iPhone 11 Pro Max = 350.3333...
//
// Just be aware of this and provide proper logic.
//
let horizontalFix = abs(proxy.size.width.distance(to: preferredWidth)).isLessThanOrEqualTo(1.0)
let verticalFix = abs(proxy.size.height.distance(to: preferredHeight)).isLessThanOrEqualTo(1.0)
switch (horizontalFix, verticalFix) {
case (true, _):
// Horizontal fix -> use preferred (= max) width -> one line of text
// Vertical is irrelevant, because we do prefer horizontal grow
return CGSize(width: preferredWidth, height: preferredHeight)
case (false, false):
// Use the offered size -> fits container, possible truncation
return proxy.size
case (false, true):
// Vertical fix -> use offered width & calculate height
return CGSize(width: proxy.size.width, height: squareArea / proxy.size.width)
}
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 50) {
Group {
TextLike(squareArea: 7000)
.fixedSize(horizontal: false, vertical: false)
TextLike(squareArea: 7000)
.fixedSize(horizontal: true, vertical: false)
TextLike(squareArea: 7000)
.fixedSize(horizontal: false, vertical: true)
TextLike(squareArea: 7000)
.fixedSize(horizontal: true, vertical: true)
}
.frame(width: 75, height: 75)
.background(Color.blue)
}
}
}
TextLike
acts as a wrapper and the actual view is in it (one additional layer).background
modifier on it for example.Let's modify the code and use the background
modifier on the TextLike
level like you did.
struct ContentView: View {
var body: some View {
VStack(spacing: 50) {
Group {
TextLike(squareArea: 7000)
.fixedSize(horizontal: false, vertical: false)
TextLike(squareArea: 7000)
.fixedSize(horizontal: true, vertical: false)
TextLike(squareArea: 7000)
.fixedSize(horizontal: false, vertical: true)
TextLike(squareArea: 7000)
.fixedSize(horizontal: true, vertical: true)
}
.background(Color.red.opacity(0.2))
.frame(width: 75, height: 75)
.background(Color.blue)
}
}
}
This is what happens:
Frankly speaking, not ideal & nice, but closest I can get for now.
Upvotes: 2