Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 14 Context Menu from UIView (Not from UIButton or UIBarButtonItem)

There is an easy way to present a context menu in iOS 13/14 via UIContextMenuInteraction:

anyUIView.addInteraction(UIContextMenuInteraction(delegate: self))

The problem for me with this is that it blurs out the whole user interface. Also, this only gets invoked via a long-press/Haptic Touch.

If I do not want the blur, there are action menus. As shown here https://developer.apple.com/documentation/uikit/menus_and_shortcuts/adopting_menus_and_uiactions_in_your_user_interface

enter image description here

This seems to present without a blur, yet it only seems to attach to a UIButton or a UIBarButtonItem.

let infoButton = UIButton()
infoButton.showsMenuAsPrimaryAction = true
infoButton.menu = UIMenu(options: .displayInline, children: [])
infoButton.addAction(UIAction { [weak infoButton] (action) in
   infoButton?.menu = infoButton?.menu?.replacingChildren([new items go here...])
}, for: .menuActionTriggered)

Is there a way to attach a context menu to a UIView that invokes on long press and does not present with blur?

like image 510
Gizmodo Avatar asked Mar 26 '21 03:03

Gizmodo


2 Answers

After some experimentation I was able to remove the dimming blur, like this. You will need a utility method:

extension UIView {
    func subviews<T:UIView>(ofType WhatType:T.Type,
        recursing:Bool = true) -> [T] {
            var result = self.subviews.compactMap {$0 as? T}
            guard recursing else { return result }
            for sub in self.subviews {
                result.append(contentsOf: sub.subviews(ofType:WhatType))
            }
            return result
    }
}

Now we use a context menu interaction delegate method to find the UIVisualEffectView that is responsible for the blurring and eliminate it:

func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
    DispatchQueue.main.async {
        let v = self.view.window!.subviews(ofType:UIVisualEffectView.self)
        if let v = v.first {
            v.alpha = 0
        }
    }
}

Typical result:

enter image description here

Unfortunately there is now zero shadow at all behind the menu, but it's better than the big blur.

And of course it’s still a long press gesture. I doubt anything can be done about that! If this were a normal UILongPressGestureRecognizer you could probably locate it and shorten its minimumPressDuration, but it isn't; you have to subject yourself to the UIContextMenuInteraction rules of the road.


However, having said all that, I can think of a much better way to do this, if possible: make this UIView be a UIControl! Now it behaves like a UIControl. So for example:

class MyControl : UIControl {
    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        let config = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in
            let act = UIAction(title: "Red") { action in  }
            let act2 = UIAction(title: "Green") { action in  }
            let act3 = UIAction(title: "Blue") { action in  }
            let men = UIMenu(children: [act, act2, act3])
            return men
        })
        return config
    }
}

And:

let v = MyControl()
v.isContextMenuInteractionEnabled = true
v.showsMenuAsPrimaryAction = true
v.frame = CGRect(x: 100, y: 100, width: 200, height: 100)
v.backgroundColor = .red
self.view.addSubview(v)

And the result is that a simple tap summons the menu, which looks like this:

enter image description here

So if you can get away with that approach, I think it's much nicer.

like image 182
matt Avatar answered Oct 05 '22 22:10

matt


I can only follow up on Matt's answer – using UIControl is much easier. Although there is no native menu property, there is an easy way how to ease the contextMenuInteraction setup, just create a subclass of UIControl and pass your menu there!

class MenuControl: UIControl {
    
    var customMenu: UIMenu
    
    // MARK: Initialization
    init(menu: UIMenu) {
        self.customMenu = menu
        super.init(frame: .zero)
        isContextMenuInteractionEnabled = true
        showsMenuAsPrimaryAction = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: ContextMenu
    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] _ in
            self?.customMenu
        })
    }
}

Then you only need to provide UIMenu with UIActions like this:

let control = MenuControl(menu: customMenu)
like image 45
Ondřej Korol Avatar answered Oct 05 '22 22:10

Ondřej Korol