-3

I'm experiencing inconsistent layout behavior with SwiftUI Text when using maxHeight constraints combined with fixedSize().

I have a long text string that behaves differently depending on the maxHeight value when fixedSize() is applied:

let str1 = "1abcdefg 2abcdefg 3abcdefg 4abcdefg 5abcdefg 6abcdefg 7abcdefg 1abcdefg 2abcdefg 3abcdefg 4abcdefg 5abcdefg 6abcdefg 7abcdefg"

// Case 1: maxHeight: 55 - Text wraps BUT exceeds frame bounds
Text(str1).frame(maxWidth: 100, maxHeight: 55).background(Color.green).fixedSize()

// Case 2: maxHeight: 15 - Text truncates and respects frame bounds  
Text(str1).frame(maxWidth: 100, maxHeight: 15).background(Color.green).fixedSize()

Observed Behavior

Case 1 (maxHeight: 55):

  • Text wraps to multiple lines
  • Problem: Text's actual bounds exceed the frame's maxHeight: 55 constraint
  • The green background shows the frame boundary, but text overflows beyond it enter image description here

Case 2 (maxHeight: 15):

  • Text truncates (doesn't wrap)
  • Text bounds respect the frame's maxHeight: 15 constraint
  • No overflow occurs

enter image description here

Expected Behavior

I expect Case 1 to behave like Case 2: the text should be constrained within the specified maxHeight bounds and truncate if necessary, rather than wrapping and overflowing.

Why does SwiftUI allow text to overflow the maxHeight constraint in Case 1 but not in Case 2?

3
  • SwiftUI views are always free to choose their own size. You can give them "suggestions" using frame/fixedSize and other modifiers, but at the end of the day it is the views themselves that make the call. Commented Nov 18 at 6:47
  • Please edit your question to only ask a single question. Commented Nov 18 at 6:57
  • @Sweeper done , I have left one question only Commented Nov 18 at 7:00

1 Answer 1

2

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:

  1. 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.
  2. 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).
  3. 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?"
  4. 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).
  5. 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))
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.