Skip to content

Instantly share code, notes, and snippets.

@odonckers
Last active February 17, 2022 15:30
Show Gist options
  • Save odonckers/1a444bb10124bcfb6409df0f7b6b3e76 to your computer and use it in GitHub Desktop.
Save odonckers/1a444bb10124bcfb6409df0f7b6b3e76 to your computer and use it in GitHub Desktop.
Solution for iOS 14 Contextual Menu Button Missing in SwiftUI
//
// MenuButton.swift
// Check Register
//
// Created by Owen Donckers on 7/16/20.
// Copyright © 2020 Owen Donckers. All rights reserved.
//
import SwiftUI
@available(iOS 14.0, *)
struct MenuButton: UIViewRepresentable {
let systemImage: String
var menuTitle: String = ""
let actions: [Action]
struct Action {
let title: String
var systemImage: String? = nil
var attributes: Attributes = .default
var action: (() -> Void)? = nil
var options: Options = .default
var children: [Action]? = nil
enum Attributes {
case `default`
case destructive
fileprivate func toUI() -> UIMenuElement.Attributes {
switch self {
case .destructive:
return .destructive
default:
return .init()
}
}
}
enum Options {
case `default`
case displayInline
case destructive
fileprivate func toUI() -> UIMenu.Options {
switch self {
case .displayInline:
return .displayInline
case .destructive:
return .destructive
default:
return .init()
}
}
}
static func `default`(title: String, systemImage: String? = nil, action: @escaping () -> Void) -> Action {
Action(title: title, systemImage: systemImage, attributes: .default, action: action)
}
static func destructive(title: String, systemImage: String? = nil, action: @escaping () -> Void) -> Action {
Action(title: title, systemImage: systemImage, attributes: .destructive, action: action)
}
static func submenu(title: String, systemImage: String? = nil, options: Options = .default, children: [Action]) -> Action {
Action(title: title, systemImage: systemImage, options: options, children: children)
}
fileprivate func toUI() -> UIMenuElement {
if action != nil {
let uiAction = UIAction(
title: title,
image: systemImage != nil ? UIImage(systemName: systemImage!) : nil,
attributes: attributes.toUI()
) { _ in self.action!() }
return uiAction
} else if children != nil {
var submenuChildren: [UIMenuElement] = []
for action in children! {
submenuChildren.append(action.toUI())
}
let uiMenu = UIMenu(
title: title,
image: systemImage != nil ? UIImage(systemName: systemImage!) : nil,
options: options.toUI(),
children: submenuChildren
)
return uiMenu
} else {
fatalError("Action requires action or children")
}
}
}
func makeUIView(context: Context) -> UIButton {
UIButton()
}
func updateUIView(_ uiButton: UIButton, context: Context) {
var menuChildren: [UIMenuElement] = []
for action in actions {
menuChildren.append(action.toUI())
}
let menu = UIMenu(
title: menuTitle,
children: menuChildren
)
let uiImage = UIImage(
systemName: systemImage,
withConfiguration: UIImage.SymbolConfiguration(scale: .large)
)
uiButton.setImage(uiImage, for: .normal)
uiButton.role = .normal
uiButton.menu = menu
uiButton.showsMenuAsPrimaryAction = true
}
}
@odonckers
Copy link
Author

odonckers commented Jul 17, 2020

Example Usage:

MenuButton(
    systemImage: "ellipsis.circle",
     menuTitle: "More buttons?",
     actions: [
        .default(title: "Add", systemImage: "plus") {
            print("Add something random")
        },
        .submenu(title: "Edit...", options: .displayInline, children: [
            .default(title: "Rename", systemImage: "pencil") {
                print("Your name is no longer proper!")
            },
            .destructive(title: "Delete", systemImage: "trash") {
                print("Don't delete me, I beg you")
            }
        ])
    ]
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment