Dynamic layouts with multi-line Text views

For a dynamic layout reacting to width changes using ViewThatFits, I need a 2-line Text view where content gets wrapped exactly once, but doesn't get abbreviated. If the text is too long to fit in two lines, ViewThatFits should choose the next layout.

I tried to use .lineLimit(2, reservesSpace: true), but then ViewThatFits always takes this layout even when the text content gets too long to fit, so it gets abbreviated with "...", instead of choosing the next layout.

How else can I build a layout with a two-line Text, which does signal to ViewThatFits if the space doesn't fit the entire text so it can choose the next layout?

Answered by Frameworks Engineer in 892568022

I have something that works though it's not ideal from a performance perspective; so definitely worth filing a follow up feedback as a request to simplify this and make it cheaper:

struct ContentView: View {
    var content: String {
        "This is some text which we hope will fit on two lines"
    }

    var body: some View {
        // Reserve two lines (always)
        Text(content)
            .hidden()
            .lineLimit(2, reservesSpace: true)
            .accessibilityHidden(true)
            .overlay {
                ViewThatFits(in: .vertical) {
                    ZStack {
                        // Measurer: reports the full unwrapped height at the proposed width.
                        // Drives the fit check; never visible.
                        Text(content)
                            .fixedSize(horizontal: false, vertical: true)
                            .hidden()
                            .accessibilityHidden(true)

                        // Renderer: the actual 2-line layout.
                        Text(content)
                            .lineLimit(2, reservesSpace: true)
                    }

                    Text("Backup")
                        .lineLimit(2, reservesSpace: true)
                }
            }
    }
}

Note: It is possible you don't need the outer hidden Text if there's other things to constrain your view but this was an easy way to set of a small sample app that shows the behavior.

I have something that works though it's not ideal from a performance perspective; so definitely worth filing a follow up feedback as a request to simplify this and make it cheaper:

struct ContentView: View {
    var content: String {
        "This is some text which we hope will fit on two lines"
    }

    var body: some View {
        // Reserve two lines (always)
        Text(content)
            .hidden()
            .lineLimit(2, reservesSpace: true)
            .accessibilityHidden(true)
            .overlay {
                ViewThatFits(in: .vertical) {
                    ZStack {
                        // Measurer: reports the full unwrapped height at the proposed width.
                        // Drives the fit check; never visible.
                        Text(content)
                            .fixedSize(horizontal: false, vertical: true)
                            .hidden()
                            .accessibilityHidden(true)

                        // Renderer: the actual 2-line layout.
                        Text(content)
                            .lineLimit(2, reservesSpace: true)
                    }

                    Text("Backup")
                        .lineLimit(2, reservesSpace: true)
                }
            }
    }
}

Note: It is possible you don't need the outer hidden Text if there's other things to constrain your view but this was an easy way to set of a small sample app that shows the behavior.

I tried your idea in my sample, but it doesn't work for me: The problem is, that I need to check whether the two-line text fits horizontally side-by-side to other views, and if not use a completely different layout where the same text has the whole width and the other views are below. See this sample:

struct AmountRowView: View {
    let title: String
    let amount: String = "$ 10.00"      // could vary, doesn't matter for the problem

    var body: some View {
        let titleView = Text(title)
            .multilineTextAlignment(.leading)

        let amountView = Text(amount)
            .border(.gray)

        let shortLayout = HStack(alignment: .center) {
            titleView.border(.green)
            Spacer(minLength: 2)
            amountView
        } // green = short

        let mediumLayout = HStack(alignment: .lastTextBaseline) {
            ZStack {
                // Measurer: reports the full unwrapped height at the proposed width.
                // Drives the fit check; never visible.
                Text(title)
                    .fixedSize(horizontal: false, vertical: true)
                    .hidden()
                    .accessibilityHidden(true)

                // Renderer: the actual 2-line layout.
                Text(title)
                    .lineLimit(2, reservesSpace: true)
            }
            .border(.red)
//                .lineLimit(2, reservesSpace: true)
//                .fixedSize(horizontal: false, vertical: true)
            Spacer(minLength: 2)
            amountView
        } // red = medium

        let longLayout = VStack(alignment: .leading) {
            titleView.border(.blue)
            HStack {
                Spacer()
                amountView
            }
        } // blue = long

        ViewThatFits(in: .horizontal) {
            shortLayout
            mediumLayout
            longLayout   // <=== comment / uncomment this line, then change dynamic fontSize
        }
    }
}

struct ContentView: View {
    var body: some View {
        List {
            Section {
                // should fit in greenV when font <= AX2
                AmountRowView(title: "Short title:")

                // should fit in redV from XXlarge to AX1
                AmountRowView(title: "Medium title, more text:")

                // needs blueV with fontsize >= XXlarge, smaller font fits in redV
                AmountRowView(title: "Long title with way more text to show here:")
            } header: { Text("ViewThatFits") }
        }
    }
}

When you comment out longLayout (line 49), you'll see that mediumLayout fits for the middle row, but is not taken from ViewThatFits if longLayout is the 3rd option.

Dynamic layouts with multi-line Text views
 
 
Q