How to force tap on UIButton using RxSwift? - ios

This is how I subscribe for tap action on UIButton:
_ = mainView.loginButton.rx.tap.subscribe { _ in
//this is not called at all
}
and now I need to force that action to check if my dependencies work great, but subscribe handler is not called.
func testRouterDidCallWithError() {
let view = LoginView()
let controller = LoginViewController(view: view)
controller.loadView()
view.loginButton.sendActions(for: .touchUpInside) //here is the point
}

This works ("ReactiveX/RxSwift" ~> 4.0)
someButton.rx.tap
.bind {
debugPrint("button tapped")
}
.disposed(by: disposeBag)
Somewhere else
someButton.sendActions(for: .touchUpInside)

Related

UICollectionViewListCell and custom cell accessory in iOS 14

My issues with new collection view list cells is that I'm not able to add action handlers to a custom accessory view.
I've been trying to do the following:
protocol TappableStar: class {
func onStarTapped(_ cell: UICollectionViewCell)
}
class TranslationListCell: UICollectionViewListCell {
let starButton: UIButton = {
let starButton = UIButton()
let starImage = UIImage(systemName: "star")!.withRenderingMode(.alwaysTemplate)
starButton.setImage(starImage, for: .normal)
starButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
starButton.addTarget(self, action: #selector(starButtonPressed(_:)), for: .touchUpInside)
starButton.tintColor = .systemOrange
return starButton
}()
var translation: TranslationModel?
weak var link: TappableStar?
override init(frame: CGRect) {
super.init(frame: frame)
accessories = [.customView(configuration: .init(customView: starButton, placement: .trailing(displayed: .always)))]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func starButtonPressed(_ sender: UIButton) {
link?.onStarTapped(self)
}
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object and update it base on state
var newConfiguration = TranslationContentConfiguration().updated(for: state)
// Update any configuration parameters related to data item
newConfiguration.inputText = translation?.inputText
newConfiguration.outputText = translation?.outputText
contentConfiguration = newConfiguration
}
}
I subclass UICollectionViewListCell, create a button with target-action handler and add it to accessories array. I also have my own implementation of cell configuration.
Now, I create a protocol where I delegate action handling to my view controller (I also implemented new cell registration API and set cell.link = self).
My problem here is that my accessory button doesn't call starButtonPressed although this accessory view is responsive (it changes color when highlighted).
My idea is that there might be something wrong with the way I implement my action handling with a custom accessory but there seems to be little to none information about this new api.
Moreover, when choosing between predefined accessories, some of them have actionHandler closures of type UICellAccessory.ActionHandler but I don't seem to understand how to properly implement that.
Any ideas would be much appreciated.
iOS 14, using UIActions
Since iOS 14 we can initialise UIButton and other UIControls with primary actions. It becomes similar to handlers of native accessories. With this we can use any parametrized method we want. And parametrising is important, because usually we want to do some action with specific cell. #selector's cannot be parametrised, so we can't pass any information to method about which cell is to be updated.
But this solution works only for iOS 14+.
Creating UIAction:
let favoriteAction = UIAction(image: UIImage(systemName: "star"),
handler: { [weak self] _ in
guard let self = self else { return }
self.handleFavoriteAction(for: your_Entity)
})
Creating UIButton:
let favoriteButton = UIButton(primaryAction: favoriteAction)
Creating accessory:
let favoriteAccessory = UICellAccessory.CustomViewConfiguration(
customView: favoriteButton,
placement: .leading(displayed: .whenEditing)
)
Using
cell.accessories = [.customView(configuration: favoriteAccessory)]
I solved my issue by adding tap gesture recognizer to my accessory's custom view. So it works like this:
let customAccessory = UICellAccessory.CustomViewConfiguration(
customView: starButton,
placement: .trailing(displayed: .always))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(starButtonPressed(_:)))
customAccessory.customView.addGestureRecognizer(tapGesture)
accessories = [.customView(configuration: customAccessory)]
Haven't seen it documented anywhere so hope it helps somebody.
I followed a similar approach to yours, but instead of a UITapGestureRecognizer, I added a target to the button.
var starButton = UIButton(type: .contactAdd)
starButton.addTarget(self, action: #selector(self.starButtonTapped), for: .touchUpInside)
let customAccessory = UICellAccessory.CustomViewConfiguration(customView: starButton, placement: .trailing(displayed: .always))
cell.accessories = [.customView(configuration: customAccessory)]
I first tried the tap gesture recognizer and it didn't work for me.

How can I subscribe to UIButton isSelected changes using RxSwift?

I am trying to have notifications when a button changes his 'isSelected' state.
I have an UIButton setup with a target to change his isSelected:
let button = UIButton()
button.addTarget(button, action: #selector(toggleSelected), for: .touchUpInside)
....
#objc func toggleSelected() {
self.isSelected = !self.isSelected
}
and I want to have something like button.rx.isSelectedChanged to be able to subscribe events.
I tried to extend UIButton.rx with a ControlProperty:
extension Reactive where Base: UIButton {
public var isSelectedChanged: ControlProperty<Bool> {
return base.rx.controlProperty(
editingEvents: [.allEditingEvents, .valueChanged],
getter: { $0.isSelected },
setter: { $0.isSelected = $1 })
}
}
and something like:
button.rx.observe(Bool.self, "isSelected")
.takeUntil(button.rx.deallocated)
.subscribe(onNext: { print("selcted \($0)") })
but in neither way I have button.isSelected updates.
What is a good way of doing this?
EDIT:
Following #daniel-t reply I implemented it here.
There's no good way of doing what you want and frankly, it's a mistake to try. The selected status of your button is changed programmatically for some reason. Don't try to store your model state in the UI, you should have the UI reflect your model state.
Look to whatever calls toggleSelected(). Likely that can be converted to rx and you can subscribe to it instead.

How to get which button is clicked in RxSwift

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

RxSwift: How to add gesture to UILabel?

I have a label with isUserInteractionEnabled set to true. Now, I need to add UITapGestureRecognizer for the label. Is there a way to add in Rx way.
I have looked at the RxSwift library here. Which they didn't provide any extension for adding gesture. The UILabel+Rx file has only text and attributedText.
Is there any workaround to add gesture to label?
A UILabel is not configured with a tap gesture recognizer out of the box, that's why RxCocoa does not provide the means to listen to a gesture directly on the label. You will have to add the gesture recognizer yourself. Then you can use Rx to observe events from the recognizer, like so:
let disposeBag = DisposeBag()
let label = UILabel()
label.text = "Hello World!"
let tapGesture = UITapGestureRecognizer()
label.addGestureRecognizer(tapGesture)
tapGesture.rx.event.bind(onNext: { recognizer in
print("touches: \(recognizer.numberOfTouches)") //or whatever you like
}).disposed(by: disposeBag)
Swift 5 (using RxGesture library).
Best and simplest option imho.
label
.rx
.tapGesture()
.when(.recognized) // This is important!
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.doWhatYouNeedToDo()
})
.disposed(by: disposeBag)
Take care! If you don't use .when(.recognized) the tap gesture will fire as soon as your label is initialised!
Swift 4 with RxCocoa + RxSwift + RxGesture
let disposeBag = DisposeBag()
let myView = UIView()
myView.rx
.longPressGesture(numberOfTouchesRequired: 1,
numberOfTapsRequired: 0,
minimumPressDuration: 0.01,
allowableMovement: 1.0)
.when(.began, .changed, .ended)
.subscribe(onNext: { pan in
let view = pan.view
let location = pan.location(in: view)
switch pan.state {
case .began:
print("began")
case .changed:
print("changed \(location)")
case .ended:
print("ended")
default:
break
}
}).disposed(by bag)
or
myView.rx
.gesture(.tap(), .pan(), .swipe([.up, .down]))
.subscribe({ onNext: gesture in
switch gesture {
case .tap: // Do something
case .pan: // Do something
case .swipeUp: // Do something
default: break
}
}).disposed(by: bag)
or event clever, to return an event. i.e string
var buttons: Observable<[UIButton]>!
let stringEvents = buttons
.flatMapLatest({ Observable.merge($0.map({ button in
return button.rx.tapGesture().when(.recognized)
.map({ _ in return "tap" })
}) )
})
Those extensions are technically part of the RxCocoa libary which is currently packaged with RxSwift.
You should be able to add the UITapGestureRecognizer to the view then just use the rx.event (rx_event if older) on that gesture object.
If you have to do this in the context of the UILabel, then you might need to wrap it inside the UILabel+Rx too, but if you have simpler requirements just using the rx.event on the gesture should be a good workaround.
You can subscribe label to the tap gesture
label
.rx
.tapGesture()
.subscribe(onNext: { _ in
print("tap")
}).disposed(by: disposeBag)
As Write George Quentin. All work.
view.rx
.longPressGesture(configuration: { gestureRecognizer, delegate in
gestureRecognizer.numberOfTouchesRequired = 1
gestureRecognizer.numberOfTapsRequired = 0
gestureRecognizer.minimumPressDuration = 0.01
gestureRecognizer.allowableMovement = 1.0
})
.when(.began, .changed, .ended)
.subscribe(onNext: { pan in
let view = pan.view
let location = pan.location(in: view)
switch pan.state {
case .began:
print(":DEBUG:began")
case .changed:
print(":DEBUG:changed \(location)")
case .ended:
print(":DEBUG:end \(location)")
nextStep()
default:
break
}
})
.disposed(by: stepBag)
I simple use this extension to get the tap as Driver in UI layer.
public extension Reactive where Base: RxGestureView {
func justTap() -> Driver<Void> {
return tapGesture()
.when(.recognized)
.map{ _ in }
.asDriver { _ in
return Driver.empty()
}
}
}
When I need the tap event I call this
view.rx.justTap()

cosmicmind material swift MenuView not closing

I am using cosmicmind material swift library and am following the examples code to try to get the FAB MenuView working.
I have copied the code and added the buttons i want, to test i am just testing with 2 buttons. The problem I am facing is with the handleMenu function:
/// Handle the menuView touch event.
internal func handleMenu() {
if menuView.menu.opened {
menuView.close()
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0))
} else {
menuView.menu.open() { (v: UIView) in
(v as? MaterialButton)?.pulse()
}
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0.125))
}
}
The full code for this UINavigationController:
import UIKit
import Material
class MyTeeUpsController: UINavigationController {
/// MenuView reference.
private lazy var menuView: MenuView = MenuView()
/// Default spacing size
let spacing: CGFloat = 16
/// Diameter for FabButtons.
let diameter: CGFloat = 56
/// Handle the menuView touch event.
internal func handleMenu() {
if menuView.menu.opened {
menuView.close()
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0))
} else {
menuView.menu.open() { (v: UIView) in
(v as? MaterialButton)?.pulse()
}
(menuView.menu.views?.first as? MaterialButton)?.animate(MaterialAnimation.rotate(rotation: 0.125))
}
}
/// Handle the menuView touch event.
internal func handleButton(button: UIButton) {
print("Hit Button \(button)")
}
private func prepareMenuView() {
//let w: CGFloat = 52
var img:UIImage? = MaterialIcon.cm.add?.imageWithRenderingMode(.AlwaysTemplate)
let button1: FabButton = FabButton()//frame: CGRectMake((view.bounds.width - w)-10, 550,w,w))
button1.setImage(img, forState: .Normal)
button1.setImage(img, forState: .Highlighted)
button1.pulseColor = MaterialColor.blue.accent3
button1.backgroundColor = MaterialColor.blueGrey.lighten1
button1.borderColor = MaterialColor.blue.accent3
button1.borderWidth = 1
button1.addTarget(self, action: #selector(handleMenu), forControlEvents: .TouchUpInside)
menuView.addSubview(button1)
img = UIImage(named: "filing_cabinet")?.imageWithRenderingMode(.AlwaysTemplate)
let button2:FabButton = FabButton()
button2.depth = .None
button2.setImage(img, forState: .Normal)
button2.setImage(img, forState: .Highlighted)
button2.pulseColor = MaterialColor.blue.accent3
button2.borderColor = MaterialColor.blue.accent3
button2.borderWidth = 1
button2.backgroundColor = MaterialColor.blueGrey.lighten1
button2.addTarget(self, action: #selector(handleButton), forControlEvents: .TouchUpInside)
menuView.addSubview(button2)
menuView.menu.direction = .Up
menuView.menu.baseSize = CGSizeMake(diameter, diameter)
menuView.menu.views = [button1,button2]
view.layout(menuView).width(diameter).height(diameter).bottomRight(bottom: 58, right: 20)
}
private func prepareTabBarItem() {
//todo
}
override func viewDidLoad() {
super.viewDidLoad()
prepareMenuView()
}
}
The menu I have embedded as a subView of UINavigationController. The reason I have added to this subView is because the FAB is on top of a search/display controller (TableView) and this way the FAB can remain on top of the TableView even when scrolling the contents of the Table.
When the view initially loads, I can click on the menu button and the animation happens correctly and button2 appears. However, it does not allow me to hit the second button OR close the menu by pressing button1 again UNLESS I navigate to another tab in the tab bar controller and then navigate back to the tab where the FAB MenuView was located. I am loading my prepareMenuView() function in viewDidLoad just as it is shown in the example.
Not sure how to modify this so that it can behave as desired. It doesn't make sense to pick another ViewController lifecycle method to run prepareMenuView().
so the issue with your code is that button2 only has the selector handler for handleButton. The handleMenu handler is not added to it. So you have two solutions.
Add the handleMenu call to the handleButton
internal func handleButton(button: UIButton) {
print("Hit Button \(button)")
handleMenu(button)
}
Add a selector handler to the button2 instance for handleMenu.
button2.addTarget(self, action: #selector(handleMenu), forControlEvents: .TouchUpInside)
button2.addTarget(self, action: #selector(handleButton), forControlEvents: .TouchUpInside)
Either option will work, just remember that order matters. So if you want the menu to close before you load some content, then call the method before or add the selector handler handleMenu before you add the handleButton.
:) All the best!

Resources