swiftuicontextmenu

Sizing of SwiftUI's contextMenu preview


I've got an app presenting some messages. I'm trying to add a contextMenu on those messages, so holding on one of them presents a larger version of the avatar and options such as showing the user's profile. I'm using .contextMenu {} preview: {} for this. The previews are giving me trouble, though. I can't seem to get the size to match the content.

Take, for eample, this:

Short text, big preview

The content is very short here. That should make the text view smaller, which should bubble up to the preview. But it doesn't happen.

With a medium length string, it seems to match, but this is probably just conincidence:

Medium text, medium preview

When I do a longer string, it fails to expand and the text is truncated:

Longer string, medium preview

I'm not sure what's going on here. At first, I thought there must be a missing relationship between the size of the view I'm providing and its parent was missing, but the behaviour with the small content seems to show there's some relationship there.

(I'm aware my view isn't perfect, in that it wouldn't currently allow text larger than the photo. I'm going to solve that after, if I get this working… unless that's the problem, but I've fixed that temporarily and it hasn't helped. It made the code more complicated, so I've left it out here.)

How do I get the preview size to match my content?

Full project (just this code, the @main app, and some resources) is on github here: https://github.com/tewha/PreviewConfusion/ if you want to poke at it.

But the view code is below:

import SwiftUI

struct Message: Identifiable {
    var id: UUID
    var title: String
    let text: String

    init(_ title: String, text: String) {
        self.id = UUID()
        self.title = title
        self.text = text
    }
}

struct ContentView: View {
    @State var presenting: Bool = false

    let messages = [
        Message("Short", text: "Is this thing even on?"),
        Message("Medium", text: "Can you hear me? I can’t hear you. Give me a minute to set up my headphones."),
        Message("Long", text: "Can you hear me? I can’t hear you. Give me a minute to set up my headphones. I still can't hear you.")
    ]
    var body: some View {
        VStack(spacing: 10) {
            Text("Hold down on text to show context menu.")
            ForEach(messages) { message in
                Button {
                    presenting = true
                } label: {
                    Text(message.title)
                }

                // Relevant code here (I think).
                .contextMenu {
                    Button {
                        // nothing to do
                    } label: {
                        Label("View Profile", systemImage: "person.circle")
                    }
                } preview: {
                    // The preview view.
                    HStack(spacing: 10) {
                        Image("Person1")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(minWidth: 120, maxWidth: 120)
                        VStack {
                            HStack {
                                Text("“\(message.text)”")
                                Spacer()
                            }
                            HStack {
                                Spacer()
                                Text(" – Person1").italic()
                            }
                        }
                    }
                    .padding(5)
                    .background(Color("Person1"))
                }
            }
        }
        .padding()
        .alert("Hold down instead of tapping.", isPresented: $presenting) {
            Button("OK", role: .cancel) {
                // alert closes automatically
            }
        }
    }
}

Solution

  • My guess is that the preview is applying (the equivalent of) .fixedSize, before then scaling the content to fit. This means, it is using the ideal size of the content to determine the space needed.

    I found that it helps to apply a sensible idealwidth to the content. For the image, it is also important to apply a maxWidth.

    .contextMenu {
        // ...
    } preview: {
        HStack(spacing: 10) {
            Image("Person1")
                .resizable()
                .scaledToFit()
                .frame(idealWidth: 100, maxWidth: 100)
            VStack(alignment: .leading) {
                Text("“\(message.text)”")
                    .frame(idealWidth: 250)
                Text(" – Person1").italic()
                    .frame(maxWidth: .infinity, alignment: .trailing)
            }
        }
        .padding(10)
        .background(Color("Person1"))
    }
    

    Screenshot

    Screenshot

    Screenshot

    You may find that very long text still gets truncated eventually, probably when the height of the preview reaches a threshold. But it is possible to show quite a lot:

    Screenshot