TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Constraining text to wrap to image width?

Forums > SwiftUI

I’m trying to implement a macOS window of fixed size that's only as big as it needs to be to contain an image with some multi-line text below it. I posted this question to Stack Overflow, and one of the proposed answers struck me as bonkers: using a GeometryReader on the image, a size preference, and some state to communicate the image size to the text. Surely there’s a more elegant solution to what seems like a very reasonable desired layout?

The following code generates this layout. I’d like for the text to wrap at the width of the image, and take up as much vertical space as it needs. The resulting window should not be resizable (so I’m using .windowResizability(.contentSize) on the Window scene).

I’ve tried various combinations of .fixedSize() and .layoutPriority(), but they seem to have no effect. Back in the UIKit days I'd set the horizontal content compression resistance priority lower.

I’m hoping to do this without resorting to alignment guides or GeometryReader. I feel like it should be possible to have text wrap in this scenario.

Screenshot of Xcode preview showing an image of a keyboard with lorem ipsum text below; the text isn’t wrapping and is wider than the image.

struct ContentView: View
{
    var body: some View
    {
        VStack
        {
            Image("keyboard")
            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
                .font(.system(size: 24.0))
                .multilineTextAlignment(.leading)
        }
        .fixedSize()
    }
}

#Preview
{
    ContentView()
}

3      

It didn’t work without a geometry reader 🤷🏻‍ Maybe the guys can help. Or does Kavsoft have a similar thread in the video?

struct ContentView: View {

    @State private var size: CGFloat = 0.0

    var body: some View {
        VStack(alignment: .center) {
            Image.hotelImage
                .coordinateSpace(name: "image")
            Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
                .font(.system(size: 24.0))
                .multilineTextAlignment(.leading)
                .frame(maxWidth: size, alignment: .leading)
                .border(.black)
        }
        .overlay(
            GeometryReader { proxy in
                Color.clear
                    .preference(key: OffsetKey.self,
                                value: proxy.frame(in: .named("image")))
            }
                .onPreferenceChange(OffsetKey.self) {
                    self.size = $0.width
                }
        )
    }
}
struct OffsetKey: PreferenceKey {
    static var defaultValue: CGRect = .zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

3      

I tried to experiment with this for a little while, but this is the closest I have come to a solution so far.

(I created TestImageView to use for my experiment, since I don't have the actual image that you are using.)

import SwiftUI

struct ContentView: View {
    let testString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

    var body: some View {
        VStack {
            TestImageView()

            Text(testString)
                .font(.system(size: 24.0))
                .multilineTextAlignment(.leading)
                .frame(minWidth: 0, idealWidth: 100, maxWidth: .infinity)
        }
        .fixedSize()
    }
}

struct TestImageView: View {
    var body: some View {
        Image(systemName: "globe")
            .resizable()
            .frame(minWidth: 100, idealWidth: 300, maxWidth: .infinity, minHeight: 100, idealHeight: 250, maxHeight: .infinity, alignment: .center)
            .foregroundColor(.blue)
            .padding()
            .background(.orange)
    }
}

This appears to almost work, but it has a hidden problem that you can see by adjusting the idealWidth on the frame of the Text view. As you make the ideal width narrower, the idealHeight of the view automatically expands, as if to be able to fit the additional lines of text that would be required to fit all of the text on the screen at that width. So, when the idealWidth is set to 50, the idealHeight increases, but if you set it to something closer to the width of the image, like 275, the idealHeight of the view decreases. As long as the idealWidth of the Text is lower than the idealWidth of the Image, the width of the text displayed won't actually change, because we are using .fixedSize() on the VStack. But it still expands the height of the Text view to make room for the extra lines of text anyway.

The only way I can see to fix this problem so far is to explicitly set the idealHeight of the Text view instead...

Text(testString)
    .font(.system(size: 24.0))
    .multilineTextAlignment(.leading)
    .frame(minWidth: 0, idealWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: 150, maxHeight: .infinity)

This allows us to set the idealWidth of the Text all the way to zero with no actual change to the width that the Text is displayed in. Again, because we are using fixedSize() on the VStack, and the idealWidth of the Image is largest, that is the width that will be used by everything in the VStack. But, this will also mean that the content of Text is no longer guaranteed to be fully displayed height-wise, or might take up extra empty space on the screen, depending on how long the text string is, and how high you set the idealHeight.

Maybe I'll have some more time to look into this further later.

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.