How to get which button is clicked in RxSwift - ios

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)
}

Related

How to modify UIMenu before it's shown to support dynamic actions

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)
}

How to find index of selected item within a UIStackView Swift 4

I have 5 buttons within a UIStackView, and I want to find out which index is being selected, and later compare those indexes. My code right now gives me an Array.Index. I've tried both subviews and arrangedSubviews. Is there anyway I can turn this into an Integer? I can't figure it out. Thanks!!
if let selectedIndex = stackview.subviews.index(of: sender) {
}
// UPDATE
I kinda got what I wanted with:
let int = stackview.subviews.distance(from: stackview.subviews.startIndex, to: selectedIndex)
I'm still not sure if this is the most efficient way, but it does the job for now.
index(of:) return Int.
Also you should find your button in the arrangedSubviews, not in the subviews
Assuming your stack view contains only buttons, and each button is connected to this #IBAction, this should work:
#IBAction func didTap(_ sender: Any) {
// make sure the sender is a button
guard let btn = sender as? UIButton else { return }
// make sure the button's superview is a stack view
guard let stack = btn.superview as? UIStackView else { return }
// get the array of arranged subviews
let theArray = stack.arrangedSubviews
get the "index" of the tapped button
if let idx = theArray.index(of: btn) {
print(idx)
} else {
print("Should never fail...")
}
}
I would add a tag to each of your buttons (button.tag = index) then check the tag of your sender.
So then you can wire up each of your buttons to the same function with a sender parameter, then check if sender.tag == index.

I've created a series of buttons programmatically. How can I recognize them when clicked?

I have a method which generates several buttons, all with the same properties. The buttons are arranged vertically, like that:
What I need is that when each of them is clicked, it is capable of recognizing if it is the first one from the top, or the second one etc., printing "I'am the first/second/etc one".
How can I do it considering that the method they have is the same?
If all you need is an integer, use the tag property in UIView.
If you have an array of buttons…
var buttons: [UIButton]
you could sort them by their position in their superview to determine their order:
var sortedButtons: [UIButton] {
return buttons.sorted(by: { $0.frame.origin.y < $1.frame.origin.y })
}
and then get the index of a button (and vice-versa):
func index(of button: UIButton) -> Int {
return sortedButtons.index(of: button)
}
func button(at index: Int) -> UIButton? {
guard index < buttons.count else { return nil }
return sortedButtons[index]
}
Then…
#IBAction func buttonClicked(_ sender: UIButton) {
guard let index = index(of: sender) else { return }
sender.setTitle("I am \(index)", for: .normal)
}
This would also work if you added your buttons to an Outlet Collection in a storyboard:
#IBoutlet var buttons: [UIButton]!
Quick and dirty approach:
When dragging the outlet for the button, for the connection type select "Outlet Collection". This will create an outlet "to" a button array. Then you can drag and drop the rest of your buttons to this array. The order you make the drag n drop will be the order of your buttons in the array.
Then you can easily access them by index.
Caution: Easily breakable if re-drag n dropping the collection.

Remove items from UIMenuController using Swift

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")
}

How to loop through all UIButtons in my Swift view?

How would I loop through all UIButtons in my view in Swift? I would want to set all the titles to "", but my for-loop in Swift is giving an error.
for btns in self.view as [UIButton] {
// set the title to ""
}
This code should work:
for view in self.view.subviews as [UIView] {
if let btn = view as? UIButton {
btn.setTitleForAllStates("")
}
}
You need to iterate through the subViews array.
Shortened and updated for Swift 3 & 4
for case let button as UIButton in self.view.subviews {
button.setTitleForAllStates("")
}
Looping over subview works, but it's sometimes a little ugly, and has other issues.
If you need to loop over some specific buttons, for example to add corner radius or change tint or background color, you can use an array of IBOutlets and then loop over that.
var buttons = [SkipBtn, AllowBtn]
for button in buttons as! [UIButton] {
button.layer.cornerRadius = 5
}
Swift 4:
let subviewButtons = self.view.subviews.filter({$0.isKind(of: UIButton.self)})
for button in subviewButtons {
//do something
}
To add some context for a common use case, suppose the buttons were in a scroll view and you wanted to highlight the tapped button and de-highlight the other buttons. In this situation, you would direct all buttons to one action method:
#objc private func buttonAction(_ button: UIButton) {
for case let b as UIButton in view.scrollView.subviews {
if b == button {
b.setTitleColor(UIColor.green, for: []) // highlight
} else {
b.setTitleColor(UIColor.black, for: []) // de-highlight
}
}
}
This code seems to be quite useful for iterating over any object within a view, just change UIButton for any other subview type such as UIView or UIImageView, etc.
let filteredSubviews = self.view.subviews.filter({
$0.isKindOfClass(UIButton)})
for view in filteredSubviews {
//Do something
}
Used some of the offered questions out there and created my own. I believe is the most efficient when you want to programmatically set up the title of various UIButtons(in my case I am building a quiz)
By randomising my array list and with just a for loop I printing the item at index to the button title
for view in self.viewForButtons.subviews{
if view.isKindOfClass(UIButton)
{
let button : UIButton = view as! UIButton
button.setTitle("item[i]", forState: .Normal)
}
}
If you have UIView's within self.view then you need to loop through the subviews while searching for UIButton. Using the accepted answer, I made this little function to do so:
Swift 4 + :
func findButton(`in` view: UIView){
for view in view.subviews as [UIView] {
if let button = view as? UIButton {
// Do something with 'button'
}else{
// Loop through subview looking for buttons
findButton(in: view)
}
}
}
Usage:
override func viewDidLoad() {
findButton(in: self.view)
}
Hope this helps!
Here's a short way in Swift if you know the subview only has buttons:
myView.subviews.map {
($0 as? UIButton)!.enabled = false
}

Resources