SwiftUI views are always free to choose their own size based on size proposals (ProposedViewSize). You can affect what proposals are sent to the views using frame/fixedSize and other modifiers, but at the end of the day it is the views themselves that make the call, and doesn't have to follow the proposals.
What happens in the maxHeight: 55 case is the following:
- The text first receives a (nil, nil) proposal. This is the
unspecified proposal, asking the view for its ideal width and height, without any limits on width/height. This is exactly what fixedSize() does.
- The text responds to (nil, nil) with a very large width and a very small height, since the text is all in one line. Let's suppose it responded with (200, 20).
- Since a width of 200 is larger than the max width set by the
frame, another proposal, (100, nil), is sent to the text, effectively asking "what is your size if you were limited to a height of 100?"
- The text is able to wrap, so it returns the same width as proposed, but with a larger height than last time. Let's suppose it returned (100, 200).
- No more proposals are sent to the text, and (100, 200) ends up being the size it chose.
In the case of maxHeight: 15, the only difference is that the height returned by the text in step 2 also exceeds the max height set by the frame. So instead of sending (100, nil) as the second proposal, it sends (100, 15). While the text's ideal height (20) is still larger than the proposal, the text tries its best to fit in by truncating to one line, even if that one line is still taller than 20pt.
Now you might be wondering why frame does not send (100, 15) as a third proposal after the text returned a size of (100, 200). I cannot answer that, but if that's what it did, then fixedSize would be quite useless after a frame.
The purpose of fixedSize is to let a view use its ideal size, whatever that might be. I would say combining it with frame is rather odd.
If you want to precisely control what proposals are sent to SwiftUI views, you can try writing a custom Layout.
To see the size proposals for yourself, you can write a UIViewRepresentable and log the proposals in sizeThatFits.
struct ProposalLogger: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIView, context: Context) -> CGSize? {
print(proposal)
// we will first receive a (nil, nil) proposal due to fixedSize()
if proposal.width == nil, proposal.height == nil {
// Text returns a size where everything is on one line
// large width, small height
return CGSize(width: 200, height: 20)
}
// then, we receive a (100, nil) proposal
else if let width = proposal.width {
// Text returns a size that fits the width,
// but of course with a larger height
return CGSize(width: width, height: 200)
} else {
// this branch will not be reached
return .zero
}
}
}
For
ProposalLogger()
.frame(maxWidth: 100, maxHeight: 55).fixedSize()
it prints:
ProposedViewSize(width: nil, height: nil)
ProposedViewSize(width: Optional(100.0), height: nil)
For
ProposalLogger()
.frame(maxWidth: 100, maxHeight: 15).fixedSize()
it prints:
ProposedViewSize(width: nil, height: nil)
ProposedViewSize(width: Optional(100.0), height: Optional(15.0))
frame/fixedSizeand other modifiers, but at the end of the day it is the views themselves that make the call.