iosswiftswiftuimenuipados

Inline Horizontal Buttons in SwiftUI Menu


In SwiftUI, there is a thing called a Menu, and in it you can have Buttons, Dividers, other Menus, etc. Here's an example of one I'm building below:

import SwiftUI

func testing() {
    print("Hello")
}

struct ContentView: View {
    var body: some View {
        VStack {
            Menu {
                Button(action: testing) {
                    Label("Button 1", systemImage: "pencil.tip.crop.circle.badge.plus")
                }
                Button(action: testing) {
                    Label("Button 2", systemImage: "doc")
                }
            }
        label: {
            Label("", systemImage: "ellipsis.circle")
        }
        }
    }
}

So, in the SwiftUI Playgrounds app, they have this menu:

enter image description here

My question is:

How did they make the circled menu option? I’ve found a few other cases of this horizontal set of buttons in a Menu, like this one below:

enter image description here

HStacks and other obvious attempts have all failed. I’ve looked at adding a MenuStyle, but the Apple’s docs on that are very lacking, only showing an example of adding a red border to the menu button. Not sure that’s the right path anyway.

I’ve only been able to get Dividers() and Buttons() to show up in the Menu:

enter image description here

I’ve also only been able to find code examples that show those two, despite seeing examples of other options in Apps out there.


Solution

  • It looks as this is only available in UIKit at present (and only iOS 16+), by setting

    menu.preferredElementSize = .medium
    

    To add this to your app you can add a UIMenu to UIButton and then use UIHostingController to add it to your SwiftUI app.

    Here's an example implementation:

    Subclass a UIButton

    class MenuButton: UIButton {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            let inspectAction = self.inspectAction()
            let duplicateAction = self.duplicateAction()
            let deleteAction = self.deleteAction()
    
            setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
            menu = UIMenu(title: "", children: [inspectAction, duplicateAction, deleteAction])
            menu?.preferredElementSize = .medium
            showsMenuAsPrimaryAction = true
    
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func inspectAction() -> UIAction {
            UIAction(title: "Inspect",
                     image: UIImage(systemName: "arrow.up.square")) { action in
               //
            }
        }
            
        func duplicateAction() -> UIAction {
            UIAction(title: "Duplicate",
                     image: UIImage(systemName: "plus.square.on.square")) { action in
               //
            }
        }
            
        func deleteAction() -> UIAction {
            UIAction(title: "Delete",
                     image: UIImage(systemName: "trash"),
                attributes: .destructive) { action in
               //
            }
        }
    
    }
    

    Create a Menu using UIViewRepresentable

    struct Menu: UIViewRepresentable {
        func makeUIView(context: Context) -> MenuButton {
            MenuButton(frame: .zero)
        }
        
        func updateUIView(_ uiView: MenuButton, context: Context) {
        }
    }
    
    

    Works like a charm!

    struct ContentView: View {
        var body: some View {
            Menu()
        }
    }
    

    enter image description here