I want to know how to do two things:
Remove the UIMenuController entirely. I want the text in WKWebView to be selected, but the UIMenuController is causing issues when the long press is released.
Add a new items to the list of menu items, and remove others.
Right now I can add an item to the end of the list, but nothing else, using the following code.
func enableCustomMenu() {
let lookup = UIMenuItem(title: "Add comment", action: "addComment")
UIMenuController.sharedMenuController().menuItems = [lookup]
}
func disableCustomMenu() {
UIMenuController.sharedMenuController().menuItems = nil
}
func addComment() {
//let text = self.webView.("window.getSelection().toString();")
print("Add Comment")
}
Related
I'm using BulletinBoard (BLTNBoard) to create dialogs in my iOS app. There's an option to embed image inside it. I would like to extend it's functionality and allow user to manipulate this image using tap gesture. But eventually when I assign a gesture to it's imageView using addGestureRecognizer nothing happens.
Here's how I initiliaze bulletin and add gesture to the image:
class ViewController: UIViewController {
lazy var bulletinManager: BLTNItemManager = {
let rootItem: BLTNPageItem = BLTNPageItem(title: "")
return BLTNItemManager(rootItem: rootItem)
}()
override func viewDidLoad() {
//etc code
let bulletinManager: BLTNItemManager = {
let item = BLTNPageItem(title: "Welcome")
item.descriptionText = "Pleas welcome to my app"
item.actionButtonTitle = "Go"
item.alternativeButtonTitle = "Try to tap here"
item.requiresCloseButton = false
item.isDismissable = false
item.actionHandler = { item in
self.bulletinManager.dismissBulletin()
}
item.alternativeHandler = { item in
//do nothing by now
}
//
item.image = UIImage(named: "welcome")
//adding gesture to its imageView
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
return BLTNItemManager(rootItem: item)
}()
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
and nothing happens at all (no message printed in console).
However if I assign action inside alternative button it works as expected:
item.alternativeHandler = { item in
item.imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: Selector("tapTap:"))
item.imageView?.addGestureRecognizer(tap)
}
I guess the only thing which can prevent me to assign the tap event to it properly is that imageView becomes available much later than the bulletin is created (for example only when it is shown on the screen).
Could you please help and correct my code. Thanks
upd.
Ok, based on Philipp's answer I have the following solution:
class myPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
let imageView=super.imageView
imageView?.isUserInteractionEnabled=true
let tap = UITapGestureRecognizer(target: self, action: #selector(tapTap))
imageView?.addGestureRecognizer(tap)
return contentViews
}
#objc func tapTap(gestureRecognizer: UITapGestureRecognizer) {
print("TAPTAP!!!!!!")
}
}
When you're working with an open source library, it's easy to check out the source code to find the answer.
As you can see here, image setter doesn't initiate the image view.
Both makeContentViews makeArrangedSubviews (which are responsible for views initializing) doesn't have any finish notification callbacks.
Usually in such cases I had to fork the repo and add functionality by myself - then I'll make a pull request if I think this functionality may be needed by someone else.
But luckily for you the BLTNPageItem is marked open, so you can just subclass it. Override makeContentViews and add your logic there, something like this:
class YourOwnPageItem: BLTNPageItem {
override func makeContentViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView] {
let contentViews = super.makeContentViews(with: interfaceBuilder)
// configure the imageView here
return contentViews
}
}
iOS 14 adds the ability to display menus upon tapping or long pressing a UIBarButtonItem or UIButton, like so:
let menu = UIMenu(children: [UIAction(title: "Action", image: nil) { action in
//do something
}])
button.menu = menu
barButtonItem = UIBarButtonItem(title: "Show Menu", image: nil, primaryAction: nil, menu: menu)
This most often replaces action sheets (UIAlertController with actionSheet style). It's really common to have a dynamic action sheet where actions are only included or may be disabled based on some state at the time the user taps the button. But with this API, the menu is created at the time the button is created. How can you modify the menu prior to it being presented or otherwise make it dynamic to ensure the appropriate actions are available and in the proper state when it will appear?
You can store a reference to your bar button item or button and recreate the menu each time any state changes that affects the available actions in the menu. menu is a settable property so it can be changed any time after the button is created. You can also get the current menu and replace its children like so: button.menu = button.menu?.replacingChildren([])
For scenarios where you are not informed when the state changes for example, you really need to be able to update the menu right before it appears. There is a UIDeferredMenuElement API which allows the action(s) to be generated dynamically. It's a block where you call a completion handler providing an array of UIMenuElement. A placeholder with loading UI is added by the system and is replaced once you call the completion handler, so it supports asynchronous determination of menu items. However, this block is only called once and then it is cached and reused so this doesn't do what we need for this scenario. iOS 15 added a new uncached provider API which behaves the same way except the block is invoked every time the element is displayed, which is exactly what we need for this scenario.
barButtonItem.menu = UIMenu(children: [
UIDeferredMenuElement.uncached { [weak self] completion in
var actions = [UIMenuElement]()
if self?.includeTestAction == true {
actions.append(UIAction(title: "Test Action") { [weak self] action in
self?.performTestAction()
})
}
completion(actions)
}
])
Before this API existed, I did find for UIButton you can change the menu when the user touches down via target/action like so: button.addTarget(self, action: #selector(buttonTouchedDown(_:)), for: .touchDown). This worked only if showsMenuAsPrimaryAction was false so they had to long press to open the menu. I didn't find a solution for UIBarButtonItem, but you could use a UIButton as a custom view.
After some trial, I've found out that you can modify the UIButton 's .menu by setting the menu property to null first then set the new UIIMenu
here is the sample code that I made
#IBOutlet weak var button: UIButton!
func generateMenu(max: Int, isRandom: Bool = false) -> UIMenu {
let n = isRandom ? Int.random(in: 1...max) : max
print("GENERATED MENU: \(n)")
let items = (0..<n).compactMap { i -> UIAction in
UIAction(
title: "Menu \(i)",
image: nil
) {[weak self] _ in
guard let self = self else { return }
self.button.menu = nil // HERE
self.button.menu = self.generateMenu(max: 10, isRandom: true)
print("Tap")
}
}
let m = UIMenu(
title: "Test", image: nil,
identifier: UIMenu.Identifier(rawValue: "Hello.menu"),
options: .displayInline, children: items)
return m
}
override func viewDidLoad() {
super.viewDidLoad()
button.menu = generateMenu(max: 10)
button.showsMenuAsPrimaryAction = true
}
Found a solution for the case with UIBarButtonItem. My solution is based on Jordan H solution, but I am facing a bug - my menu update method regenerateContextMenu() was not called every time on menu appears, and I was getting irrelevant data in the menu. So I changed the code a bit:
private lazy var threePointBttn: UIButton = {
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
// pay attention on UIControl.Event in next line
$0.addTarget(self, action: #selector(regenerateContextMenu), for: .menuActionTriggered)
$0.showsMenuAsPrimaryAction = true
return $0
}(UIButton(type: .system))
override func viewDidLoad() {
super.viewDidLoad()
threePointBttn.menu = createContextMenu()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: threePointBttn)
}
private func createContextMenu() -> UIMenu {
let action1 = UIAction(title:...
// ...
return UIMenu(title: "Some title", children: [action1, action2...])
}
#objc private func regenerateContextMenu() {
threePointBttn.menu = createContextMenu()
}
tested on iOS 14.7.1
Modified Jordan H's version to separate the assignment and build action
This will build the menu on the fly every time the button is tapped
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem?.menu = UIMenu(children: [
// build menu every time the button is tapped
UIDeferredMenuElement.uncached { [weak self] completion in
if let menu = self?.buildMenu() as? UIMenu {
completion([menu])
}
}
])
}
func buildMenu() -> UIMenu {
var actions: [UIMenuElement] = []
// build actions
UIAction(title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle")) { _ in
self.filterTapped()
}
actions.append(filterAction)
return UIMenu(options: .displayInline, children: actions)
}
I have created a common action for an array of my button. I just want to get the which button is tapped.
I have array of buttons like let buttons = [UIButton(), UIButton(), UIButton(),UIButton()].
let observable = Observable.of(buttons[0].rx.tap, buttons[1].rx.tap, buttons[2].rx.tap, buttons[3].rx.tap).merge()
observable.subscribe(onNext: {
print("I want to find which button is tapped.")
}).disposed(by: disposeBag)
Just map the tap events to some custom IDs -
let observable = Observable.merge(
buttons[0].rx.tap.map { 0 },
buttons[1].rx.tap.map { 1 },
// etc.
)
observable.subscribe(onNext: { id in
print("\(id) button is tapped.")
}).disposed(by: disposeBag)
The correct answer is to not merge the buttons in the first place. If you want to do four different things, then have four different observables. If they are all doing the same thing, just with different data then simply:
let taps = buttons.enumerated().map { ($0.0, $0.1.rx.tap) }
let toInts = taps.map { index, obs in obs.map { index } }
let mergedTaps = Observable.merge(toInts)
On review, I really like an answer by #Sooraj_snr that has been deleted. Use the buttons' tags instead of their position in the array. It's much more robust.
let tags = buttons
.map { ($0.rx.tap, $0.tag) }
.map { obs, tag in obs.map { tag } }
let values = Observable.merge(tags)
You are using merge(). You can't know which on is being tapped.
If you want multiple buttons but one action you are doing the right thing.
let observable = Observable.of(buttons[0].rx.tap, buttons[1].rx.tap,
buttons[2].rx.tap, buttons[3].rx.tap).merge()
observable.subscribe(onNext: {
print("I want to find which button is tapped.")
}).disposed(by: disposeBag)
If the action is different then:
Example
Let's say I have a UIView and 4 buttons. In button tap, you change the background of the view. Different color per button. So same function but different color.
Observable.of(UIButton().rx.tap.map { _ in UIColor.red }).merge().subscribe(onNext: { color in
UIView().backgroundColor = color
})
Here we give the button tag.
buttons.enumerated().forEach { (index, button) in
button.tag = index
}
Here we got the which button we have selected.
for button in buttons {
button.rx.tap.subscribe { [weak self] event in
print("Selected Button :- \(button.tag)")
}.disposed(by: disposeBag)
}
;) hi guys!
I want delete function in MultivaluedSection,but I don't want display delete icon in section's rows, something like this:
let section2 = MultivaluedSection(multivaluedOptions: .Delete, footer: "")
for _ in 1..<4 {
section2 <<< PickerInlineRow<String> {
$0.title = "Tap to select"
$0.value = "client"
$0.options = nameList
}
}
I only want user swipe row to delete it.
I try solve it by check in Eureka source code,but can't find any method or property to do this.
FIX:
oh!I notice this code in Eureka demo->MultivaluedOnlyDeleteController->viewDidLoad:
tableView.isEditing = false
But,it seem not working at first time.user must tap edit button to reset editing status.
what's wrong with it???
To fix this issue you only have to override the viewWillAppear viewController method
override func viewWillAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.isEditing = false
}
I have a UICollectionView with two UITextView's in each cell and want to add a custom menu item, when text is selected. What I did in my CollectionViewCell-class:
override func awakeFromNib()
{
var menuController = UIMenuController.sharedMenuController()
var translateSelectionItem = UIMenuItem(title: "Translate", action: "translateSelection")
menuController.menuItems = NSArray(array: [translateSelectionItem])
}
But the item doesn't show up in the menu. Where is the problem ?
It takes more than creating the custom menu item. To make the custom menu item actually appear, the first responder must also return true from canPerformAction:withSender: for this action, and of course there must be an implementation of the action method as well.