0

Problem: Adding Menu inside List but only the text(black area) is tappable not the entire row. The source code and resulting image are shown below.

struct testMenuInList: View {
    var body: some View {
        List {
            Menu(content: {
                Button("Button 1") {
                    
                }
                Button("Button 2") {
                    
                }
                Button("Button 3") {
                    
                }
            }, label: {
                Text("Add Question")
                    .foregroundStyle(Color.white)
                    .background(Color.black)
            })
        }
    }
}

screenshot


solution1: Change the listRowInsets and the Menu label to a HStack to make the tappable black area bigger, but here comes another problem, the menu detail position shifted. The source code and resulting image are shown below.

struct testMenuInList: View {
    @Environment(\.defaultMinListRowHeight) private var rowHeight: CGFloat
    
    var body: some View {
        List {
            Menu(content: {
                Button("Button 1") {
                    
                }
                Button("Button 2") {
                    
                }
                Button("Button 3") {
                    
                }
            }, label: {
//                Text("Add Question")
//                    .foregroundStyle(Color.white)
//                    .background(Color.black)
                HStack {
                    Text("Add Question")
                        .foregroundStyle(Color.white)
                    Spacer()
                }
                .frame(height: rowHeight)
                .background(Color.black)
            })
            .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
    }
}

screenshot


Help: How to make the whole row tappable as shown in solution1 while maintain the menu detail at the position shown in problem. This is doable because I saw apple's native app did it. iphone -> App "Remainders" -> New Remainder(bottom left) -> Details -> Add Image. Do I really need to make my own custom Menu or are there native swiftui modifiers which can easily do this? screenshot

2
  • You can just click the "enter image description here" to view the images, sry I forgot to write the description. Commented Mar 29 at 6:59
  • 1
    @BenzyNeez I think I did not explained clearly, I want the menu detail(buttons) to be right on top of the Text("Add Question") as shown in problem Image instead of at the middle of the list row. Commented Mar 29 at 8:10

2 Answers 2

0

I expect that UIKit might let you control the position of the menu more precisely, but, unfortunately, you don't have much control with SwiftUI. I am guessing, the Reminders app is not implemented using SwiftUI.

For the case shown in the first block of code, you can increase the tappable area by applying a frame to the menu label and setting a content shape. This is similar to what you were doing in the potential solution you showed in the question. It can be done without an HStack:

Menu {
    // ... buttons, as before
} label: {
    Text("Add Question")
        .foregroundStyle(Color.white)
        .background(Color.black)
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
}

However, when you tap the row, the menu always aligns to the middle of the row, even when you tap on the text (as you also explained in the question):

screenshot

Here are some possible workarounds, none of which are perfect, but they might help:


1. Experiment with minWidth

In my tests on an iPhone 16 simulator, the point at which it switches from left-alignment to center-alignment is when the width of the label is greater than about 273 points. This is about two-thirds of the screen width on an iPhone 16, but it might be different on other devices. So if the frame on the label is changed from maxWidth: .infinity to, say, minWidth: 270, this gives a larger hit area with the menu still left-aligned. However, the hit area will not include the trailing area on the right of the row.

Text("Add Question")
    .foregroundStyle(Color.white)
    .background(Color.black)
    .frame(minWidth: 270, alignment: .leading) // 👈 changed
    .contentShape(Rectangle())

2. Offset the label with padding to compensate

As we have seen, when the label has maximum width, the menu is centered. It turns out that if the center of the row is shifted, the menu moves too.

The center of the row can be shifted by applying a positive x-offset to the label, then compensating with negative leading padding on the Menu.

  • By using .offset instead of leading .padding for the label, the width available for the text does not change. So a long label will only wrap if the full row width is too narrow.
  • By using negative padding for the Menu, the hit area does not move. If instead a negative offset is used, the right-side of the row is no longer receptive to taps.
private let labelOffset: CGFloat = 20
Menu {
    // ... buttons, as before
} label: {
    Text("Add Question")
        .foregroundStyle(Color.white)
        .background(Color.black)
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
        .offset(x: labelOffset) // 👈 added
}
.padding(.leading, -labelOffset) // 👈 added

When the size of the offset is small, the menu moves by half this size. For example, if the offset is 20 (as above), the menu is shown 10 points to the left of center.

There seems to be a threshold, above which the menu alignment switches from the center of the row to the left side of the row. In my tests on an iPhone 16 simulator, the threshold was around 40. This value happens to be the distance from the edge of the screen to the row content. When the list row insets are changed, the threshold also changes. So the threshold probably depends on the insets.

Increasing the offset above this threshold makes no difference. So to move the menu to the left, the offset doesn't need to be an exact amount, it just needs to be more than the threshold.

Here is how it looks using an offset of 50. It also looks exactly the same if you use an offset of 100:

Screenshot

To fine tune the positioning, it would be nice if the menu could be moved to the right by about 12 points, so that it aligns with the row. Unfortunately, I couldn't find a way to do this😢


3. Add trailing padding to the menu

As kind of a combination of the two workarounds above, the menu can also be moved to the left side by adding trailing padding to the menu.

The amount of padding that is needed corresponds again to the threshold offset discussed above for workaround 2. So with the default list insets, the padding for an iPhone 16 needs to be at least 40. On an iPad, it needs to be larger (100 works).

  • The hit area on the right-side of the row is reduced by the padding amount, so it is best to keep the padding to the minimum necessary.

  • Negative padding can be added to the label to prevent the text from wrapping, if needed. However, this doesn't help to increase the hit area.

This variant gives better menu alignment. So even though the hit area excludes the width of the padding on the right side of the row, it is a better solution than workaround 1:

Menu {
    // ... buttons, as before
} label: {
    Text("Add Question")
        .foregroundStyle(Color.white)
        .background(Color.black)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.trailing, -40) // 👈 added
        .contentShape(Rectangle())
}
.padding(.trailing, 40) // 👈 added

Screenshot

Sign up to request clarification or add additional context in comments.

1 Comment

I have tried your solutions, and yeah it solve the problem but just cannot meet my demand. I will try making my own menu using VStack.
0

I finally found a solution to this problem using UIKit together with Swiftui List.

import SwiftUI
import UIKit

struct testMenuInList: View {
    @Environment(\.defaultMinListRowHeight) private var rowHeight: CGFloat
    @State private var showMenu = false
    
    var body: some View {
        List {
            Section {
                Text("\(showMenu)")
            }
            
            Text("\(rowHeight)")
            Section {
                UIMenuView()
                    .frame(height: rowHeight, alignment: .leading)
            }
            .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
    }
}

struct UIMenuView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIButton {
        // Create a UIButton to attach the UIMenu
        let button = UIButton(type: .system)
        button.setTitle("Add Question", for: .normal)
        button.contentHorizontalAlignment = .leading // Align the text to the leading
        button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0) // Add padding to the left
        
        // Create the UIMenu with four buttons
        let menu = UIMenu(title: "", children: [
            UIAction(title: "Button 1", handler: { _ in print("Button 1 tapped") }),
            UIAction(title: "Button 2", handler: { _ in print("Button 2 tapped") }),
            UIAction(title: "Button 3", handler: { _ in print("Button 3 tapped") }),
            UIAction(title: "Button 4", handler: { _ in print("Button 4 tapped") })
        ])
        
        // Attach the menu to the button
        button.menu = menu
        button.showsMenuAsPrimaryAction = true // Show the menu when the button is tapped
        
        return button
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) {
        // No updates needed for now
    }
}

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.