Presenting Context menu on button tap in swift - swift

In iOS context menus can be shown by a long press or a tap. Currently the below code shows a context menu on a long press, how do I present the menu on a tap?
let interaction = UIContextMenuInteraction(delegate: self)
tagBtn.addInteraction(interaction)
func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint)
-> UIContextMenuConfiguration? {
let favorite = UIAction(title: "Favorite",
image: UIImage(systemName: "heart.fill")) { _ in
// Perform action
}
let share = UIAction(title: "Share",
image: UIImage(systemName: "square.and.arrow.up.fill")) { action in
// Perform action
}
let delete = UIAction(title: "Delete",
image: UIImage(systemName: "trash.fill"),
attributes: [.destructive]) { action in
// Perform action
}
return UIContextMenuConfiguration(identifier: nil,
previewProvider: nil) { _ in
UIMenu(title: "Actions", children: [favorite, share, delete])
}
}

UIContextMenuInteraction is intended only for contextual (long-press) menus.
If you want the primary action of the button to display a menu, you can just create a UIMenu and assign it directly to the button.menu property, then set button.showsMenuAsPrimaryAction = true, like this:
let favorite = UIAction(title: "Favorite",
image: UIImage(systemName: "heart.fill")) { _ in
// Perform action
}
...
let button = UIButton()
button.showsMenuAsPrimaryAction = true
button.menu = UIMenu(title: "", children: [favorite, ...])

Related

How to add checkmark to submenu of UIMenu in Swift Storyboard?

I'm trying to create a filter option for an app in Swift. Currently, the UI will add a check mark if one of the category filters are selected, such as in the image below for "Food". However, if the "Within Radius" filter is selected, the UI doesn't get updated.
At the moment, this is the code I've written to add the checkmark:
private func updateActionState(actionTitle: String? = nil, menu: UIMenu) -> UIMenu {
if let actionTitle = actionTitle {
menu.children.forEach { action in
guard let action = action as? UIAction else {
return
}
if action.title == actionTitle {
if(action.state == .on){
action.state = .off
}
else{
action.state = .on
}
}
else{
action.state = .off
}
}
} else {
let action = menu.children.first as? UIAction
action?.state = .on
}
return menu
}
I created the menu as follows:
private lazy var elements: [UIAction] = [food, clothing, furniture, other]
private lazy var menu = UIMenu(title: "Category", children: elements)
private lazy var deferredMenu = UIMenu(title: "Distance", options: .displayInline, children: [self.distanceRadius])
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
loadMap();
menu = menu.replacingChildren([food, clothing, furniture, other, deferredMenu])
navigationItem.leftBarButtonItem?.menu = menu
}
And the UIAction is declared as:
private lazy var distanceRadius = UIAction(title: "Within Radius", attributes: [], state: currFilter == "Within Radius" ? .on : .off){action in
var alert = UIAlertController(title: "Radius", message: "Filter within a radius (in miles)", preferredStyle: .alert)
//2. Add the text field. You can configure it however you need.
alert.addTextField(configurationHandler: { (textField) -> Void in
textField.text = ""
})
//3. Grab the value from the text field, and print it when the user clicks OK.
var radius = 0
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (action) -> Void in
let textField = (alert?.textFields![0])! as UITextField
radius = Int(textField.text!) ?? 0
self.toggleFilter(actionTitle: "Within Radius", radius: radius)
}))
// 4. Present the alert.
self.present(alert, animated: true, completion: nil)
self.navigationItem.leftBarButtonItem?.menu = self.updateActionState(actionTitle: "Within Radius", menu: self.menu);
}
However, for the "Within Radius" option, the action.title is "Distance", as opposed to "Within Radius", which is what the UIAction is created with. Is there any way to cast a UIAction as a UIMenu to access the children within distanceRadius?
Or is there another way to get the check mark to appear in the Distance submenu?
I've tried re-calling updateActionState on deferredMenu as well but that did not do anything.

Use action with selector in UIAction for menus in UIKit

Im trying to use action in the UIAction for my menu item in UIKit. So, For first button Im not able to apply action. It shows me error "Cannot find 'action' in scope"
I really want to use selector in this case. I want to know what's perfect way to take action on the selector
class MessageViewController : UIViewController, UITableViewDelegate {
private lazy var first = UIAction(title: "Edit", image: UIImage(systemName: "pencil.circle"), attributes: [], state: .off) { [self]_ in
action: #selector(self.RightSideBarButtonItemTapped(_:))
}
private lazy var second = UIAction(title: "Second", image: UIImage(systemName: "pencil.circle"), attributes: [.destructive], state: .on) { action in
print("Second")
#selector(self.sendMessageRightSideBarButtonItemTapped(_:))
}
private lazy var third = UIAction(title: "Third", image: UIImage(systemName: "pencil.circle"), attributes: [], state: .off) { action in
print("third")
}
private lazy var elements: [UIAction] = [first]
private lazy var menu = UIMenu(title: "new", children: elements)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: nil)
menu = menu.replacingChildren([first, second, third])
if #available(iOS 14.0, *) {
navigationItem.rightBarButtonItem?.menu = menu
}
}
Tried Solution from #Hangar Rash
private lazy var first = UIAction(title: "Edit", image: UIImage(systemName: "pencil.circle"), attributes: [], state: .off)
{ [unowned self] action in
self.RightSideBarButtonItemTapped(_:)
// Getting error on this line which says its " Function is unused "
}
override func viewDidLoad() {}
override func viewDidAppear(_ animated: Bool) {}
#objc func RightSideBarButtonItemTapped(_ sender:UIBarButtonItem!)
{
let vc = IceWorldView()
present(vc, animated: true)
}
Your first action has two issues:
a syntax error with action
there's no need to use #selector. Just call the method directly
Change:
private lazy var first = UIAction(title: "Edit", image: UIImage(systemName: "pencil.circle"), attributes: [], state: .off) { [self]_ in
action: #selector(self.RightSideBarButtonItemTapped(_:))
}
to:
private lazy var first = UIAction(title: "Edit", image: UIImage(systemName: "pencil.circle"), attributes: [], state: .off) { [unowned self] action in
self.RightSideBarButtonItemTapped(someButton)
}
Your RightSideBarButtonItemTapped expects a UIBarButtonItem parameter but you don't have one in the menu. You can either made a dummy button instance to pass in or you can change the RightSideBarButtonItemTapped so it doesn't take any parameters. You don't seem to use the passed in sender anyway.
Fix the use of #selector in your second action as well.

ContextMenu issue in UIBarButtonItem

I put UIBarButtonItem inside Toolbar as you can see in Storyboard. I put UIMenu in UIBarButtonItem.menu. When I click a button in UIMenu, the icon of the button I clicked is transferred to UIBarButtonItem. Why is this happening?
Question:
Why does the UIBarButtonItem's icon change when I press the button from ContextMenu?
Video:
Storyboard:
UIBarButtonItem Attributes:
ViewController:
#IBOutlet weak var fooItem: UIBarButtonItem!
//MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
fooItem.menu = ContextMenuManager.shared.makeUIMenu()
}
ContextMenuManager:
protocol ContextMenuFunctionable {
func didTappedEmptyFolder()
func didTappedImportFile()
}
class ContextMenuManager {
static var shared = ContextMenuManager()
var delegate: ContextMenuFunctionable?
var emptyFolder: UIAction?
var importFile: UIAction?
var importPhotoOrVideo: UIAction?
private init() {
emptyFolder = UIAction(
title: "Empty Folder",
image: .folderBadgePlus
.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedEmptyFolder() }
)
importFile = UIAction(
title: "Import File",
image: .squareAndArrowDown.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedImportFile() }
)
importPhotoOrVideo = UIAction(
title: "Import Photo & Video",
image: .photo.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedImportFile() }
)
}
func makeUIMenu() -> UIMenu {
guard let emptyFolder, let importFile, let importPhotoOrVideo else { return UIMenu() }
return UIMenu(title: "Add",
image: .folder.applyingSymbolConfiguration(.symbolConfig),
options: [.displayInline, .singleSelection],
children: [importPhotoOrVideo, importFile, emptyFolder])
}
}
Uncheck the "Selection as Primary Action" checkbox and the button's icon should no longer change when you make a menu selection.
I replicated what you are seeing in code. I created a menu basically using your ContextMenuManager code but I created the UIBarButtonItem as follows:
let menu = UIMenu(title: "Add",
options: [.displayInline, .singleSelection],
children: [importPhotoOrVideo, importFile, emptyFolder])
let button = UIBarButtonItem(image: UIImage(systemName: "doc.on.clipboard.fill"), menu: menu)
button.changesSelectionAsPrimaryAction = false
When changesSelectionAsPrimaryAction is set to true, the button's icon reflects the chosen menu. When set to false, it retains its own icon.

How to implement a NSButton like the Finder button

Please refer to the screenshot, I want to implement a NSButton like the buttons on Finder window. It seems that the button style is similar with a NSButton recessed. I have tried the recessed button but it is a little different from the Finder button, even the one which have a drop down menu. I have no idea how to make a drop down menu with recessed button.
If I choose to make a bevel style borderless NSButton with a special image, I cannot set a highlighted hover background whose size is larger than the image inside. The hover area is always the same with the image.
So any body known how to implement this. Thanks
Use NSMenuToolbarItem instead and check the inline comment in the sample source:
Assign this custom class MainWindowController to the initial NSWindowController controller:
class MainWindowController: NSWindowController {
// `NSMenuToolbarItem` Items
var dropDownMenu: NSMenu = {
var menu = NSMenu(title: "DropDown")
let item1 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item2 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item3 = NSMenuItem(title: "Item 3", action: nil, keyEquivalent: "")
menu.items = [item1, item2, item3]
return menu
}()
override func windowDidLoad() {
super.windowDidLoad()
configureToolbar()
}
private func configureToolbar() {
if let safeWindow = self.window {
let toolbar = NSToolbar(identifier: "mainWindowToolbar")
toolbar.delegate = self
toolbar.displayMode = .default
safeWindow.toolbarStyle = .automatic
safeWindow.toolbar = toolbar
safeWindow.toolbar?.validateVisibleItems()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = NSMenuToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.showsIndicator = true // To display DropDown Indicator
toolbarItem.menu = self.dropDownMenu
toolbarItem.isBordered = true // Show ToolBar Item Background on Mouse Hover
toolbarItem.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: "")
return toolbarItem
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
}
Output:

Right click on UIButton for UIKitForMac (catalyst)

Is it possible to easily enable right click on a Catalyst app within UIKitForMac?
Currently the following code works perfectly on left click, but nothing is called on right click:
button.addTarget(self, action: #selector(doSomething), for: .touchUpInside)
// Is called for left click but not for right click
#objc func doSomething(sender:UIButton, event:UIEvent) -> Bool {
https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/
This menu works by right clicking on Mac Catalyst
1. Add Iteraction
let interaction = UIContextMenuInteraction(delegate: self)
yourButton.addInteraction(interaction)
2. Add extension UIContextMenuIteractionDelegate
extension AccountViewVC: UIContextMenuInteractionDelegate {
public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { suggestedActions in
let delete = UIAction(title: "Delete", image: UIImage(systemName: "arrow")) { action in
}
let children = [delete]
return UIMenu(title: "Main Menu", children: children)
})
}}