How can I subscribe to UIButton isSelected changes using RxSwift? - ios

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.

Related

How to set different font size for UIButton when it is pressed

I am trying to change font size of button itself after it was pressed.
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
buttonToResize.titleLabel!.font = UIFont(name: "Helvetica", size: 40)
// Also tried buttonToResize.titleLabel?.font = UIFont .systemFont(ofSize: 4)
}
However the changes are not applied.
What is interesting, to me, that if I try to resize some other button (second one) after pressing on initial (first one), it works as expected.
Like this:
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBOutlet weak var secondButtonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
secondButtonToResize.titleLabel!.font = UIFont(name: "Helvetica", size: 40)
}
Other properties like backgroundColor seems to apply, however with font size I face problem.
First, here's the sequence of events when you tap on a UIButton.
The button sets its own isHighlighted property to true.
The button fires any Touch Down actions, possibly multiple times if you drag your finger around.
The button fires any Primary Action Triggered actions (like your buttonTapped).
The button fires any Touch Up actions.
The button sets its own isHighlighted property to false.
Every time isHighlighted changes, the button updates its styling to how it thinks it should look. So a moment after buttonTapped, the button you pressed overwrites your chosen font with its own font.
It's worth exploring this to make sure you understand it by creating a UIButton subclass. Don't use this in production. Once you start overriding parts of UIButton, you need to override all of it.
// This class is to demonstrate the sequence of events when you press a UIButton.
// Do not use in production.
// To make this work properly, you would also have to override all the other properties that effect UIButton.state, and also UIButton.state itself.
class MyOverrideHighlightedButton : UIButton {
// Define a local property to store our highlighted state.
var myHighlighted : Bool = false
override var isHighlighted: Bool {
get {
// Just return the existing property.
return myHighlighted
}
set {
print("Setting MyOverrideHighlightedButton.isHighlighted from \(myHighlighted) to \(newValue)")
myHighlighted = newValue
// Since the UIButton remains unaware of its highlighted state changing, we need to change its appearance here.
// Use garish colors so we can be sure it works during testing.
if (myHighlighted) {
titleLabel!.textColor = UIColor.red
} else {
titleLabel!.textColor = titleColor(for: .normal)
}
}
}
}
So where does it keep pulling its old font from? On loading a view it will apply UIAppearance settings, but those will get discarded when you press the button too. iOS 15+, it looks like it uses the new UIButton.Configuration struct. So you could put this in your buttonTapped:
// The Configuration struct used here was only defined in iOS 15 and
// will fail in earlier versions.
// See https://developer.apple.com/documentation/uikit/uibutton/configuration
sender.configuration!.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
var outgoing = incoming
// We only want to change the font, but we could change other properties here too.
outgoing.font = UIFont(name: "Zapfino", size: 20)
return outgoing
}
I'd like to think there's a simpler way to do this. Whichever way you work, make sure it will also work in the event of other changes to your button, such as some other event setting isEnabled to false on it.
You probably want something like this
struct MyView: View {
#State var pressed: Bool = false
var body: some View {
Button(action: {
withAnimation {
pressed = true
}
}) {
Text("Hello")
}
Button(action: {
}) {
Text("Hello")
.font(pressed ? .system(size: 40) : .system(size: 20))
}
}
}
class ViewController: UIViewController {
#IBOutlet weak var buttonToResize: UIButton!
#IBAction func buttonTapped(_ sender: UIButton) {
sender.titleLabel?.font = .systemFont(ofSize: 30)
}
}
This should solve the problem. Use the sender tag instead of the IBOutlet.
Cowirrie analysis made me think of this solution (tested)
#IBAction func testX(_ sender: UIButton) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
sender.titleLabel?.font = sender.titleLabel?.font.withSize(32)
}
}

Pass parameters to button action in swift

When setting the action with the "addTarget" method on a button in Swift, is there a way for me to pass a parameter to the function I want to trigger?
Say I had a simple scenario like this:
let button = UIButton()
button.addTarget(self, action: #selector(didPressButton), for: .touchUpInside)
#objc func didPressButton() {
// do something
}
Obviously the above code works fine, but say I wanted the 'didPressButton' function to take in a parameter:
#objc func didPressButton(myParam: String) {
// do something with myParam
}
Is there a way I can pass a parameter into the function in the 'addTarget' method?
Something like this:
let button = UIButton()
button.addTarget(self, action: #selector(didPressButton(myParam: "Test")), for: .touchUpInside)
#objc func didPressButton(myParam: String) {
// do something with myParam
}
I'm coming from a JavaScript background and in JavaScript it's pretty simple to achieve this behavior by simply passing an anonymous function that would then call the 'didPressButton' function. However, I can't quite figure how to achieve this with swift. Can a similar technique be used by using a closure? Or does the '#selector' keyword prevent me from doing something like that?
Thank you to anyone who can help!
The short answer is no.
The target selector mechanism only sends the target in the parameters. If the string was a part of a subclass of UIbutton then you could grab it from there.
class SomeButton: UIButton {
var someString: String
}
#objc func didPressButton(_ button: SomeButton) {
// use button.someString
}
It is not possible to do that in iOS. You can get the View in the selector in your case it is a button.
button.addTarget(self, action: #selector(buttonClick(_:)), for: .touchUpInside)
#objc func buttonClick(_ view: UIButton) {
switch view.titleLabel?.text {
case "Button":
break
default:
break
}
}

Why is the new iOS 14 UIControl action syntax so terrible?

New in iOS 14, we can attach an action handler directly to a UIControl:
let action = UIAction(title:"") { action in
print("howdy!")
}
button.addAction(action, for: .touchUpInside)
That's cool in its way, but the syntax is infuriating. I have to form the UIAction first. I have to give the UIAction a title, even though that title will never appear in the interface. Isn't there a better way?
First, you don't need to supply the title. This is (now) legal:
let action = UIAction { action in
print("howdy!")
}
button.addAction(action, for: .touchUpInside)
Second, you don't really need the separate line to define the action, so you can say this:
button.addAction(.init { action in
print("howdy!")
}, for: .touchUpInside)
However, that's still infuriating, because now I've got a closure in the middle of the addAction call. It ought to be a trailing closure! The obvious solution is an extension:
extension UIControl {
func addAction(for event: UIControl.Event, handler: #escaping UIActionHandler) {
self.addAction(UIAction(handler:handler), for:event)
}
}
Problem solved! Now I can talk the way I should have been permitted to all along:
button.addAction(for: .touchUpInside) { action in
print("howdy!")
}
[Extra info: Where's the sender in this story? It's inside the action. UIAction has a sender property. So in that code, action.sender is the UIButton.]

Cannot Disable Default UIMenuItems in UIMenuController in UITextView

I'm trying to configure UIMenuController's menu items for a functionality similar to Medium's iOS feature:
There are a variety of threads devoted to this specific task, but despite tens of thousands of views and varied results, including it not working for a significant enough number of people... it doesn't seem like there is a solution that works consistently for UITextView.
I have been able to add a custom menu option "printToConsole", but I can't disable Apple's standard menu items like cut, copy, paste, B I U, etc:
The consensus seems to be that I should override canPerformAction to disable these default menu items as such, but that doesn't seem to be working:
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
print("canPerformAction being called")
if action == #selector(cut(_:)) {
return false
}
if action == #selector(copy(_:)) {
return false
}
if action == #selector(select(_:)) {
return false
}
if action == #selector(paste(_:)) {
return false
}
if action == #selector(replacementObject(for:)) {
return false
}
if action == #selector(selectAll(_:)) {
return false
}
if action == #selector(printToConsole) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
This is the remainder of my relevant code:
func addCustomMenu() {
let consolePrintAction = UIMenuItem(title: "Print To Console", action: #selector(printToConsole))
UIMenuController.shared.menuItems = [consolePrintAction]
UIMenuController.shared.update()
}
#objc func printToConsole() {
if let range = articleTextView.selectedTextRange, let selectedText = articleTextView.text(in: range) {
print(selectedText)
}
}
And in my viewDidLoad:
articleTextView.delegate = self
addCustomMenu()
I've set my viewController to conform to UITextViewDelegate as well. Some are suggesting that if you simply subclass the TextView this will work somehow. I haven't been able to get that to work, so if that is truly the answer, can someone provide an example?
Again, I know this may seem like a duplicate, but the above solution appears to have stopped working with an update of iOS.
Thanks.
Going to give credit to #gaurav for his answer on this one, which must have escaped me in my hunt on SO: https://stackoverflow.com/a/46470592/7134142
The key piece of code is this, which is extending UITextView, rather than subclassing it:
extension UITextView {
open override func canPerformAction(_ action: Selector, withSender
sender: Any?) -> Bool {
return false
}
Overriding canPerformAction in my view controller was unnecessary, and the above code still allows you to add your custom menu items. This is what I ended up with:

How to perform selector with a nested function?

I have a nested function like this and I want to call childFunc when user tap to my button, but it does not work
class MyClass {
func parentFunc() {
button.addTarget(self, action: #selector(parentFunc().childFunc), for: .touchUpInside)
func childFunc() {
print("User tapped")
}
}
}
It raise error like this:
Value of tuple type '()' has no member 'childFunc'
Is there any way to perform childFunc with #selector ?
Edit 1:
I have use closure like this, But I think it's not a nice way because I have to make another function
class MyClass {
myClosure: () -> () = {}
func parentFunc() {
button.addTarget(self, action: #selector(runChildFunc), for: .touchUpInside)
func childFunc() {
print("User tapped")
}
myClosesure = childFunc
}
func runChildFunc() {
myClosure()
}
}
instead, you can try below code to achieve your goal
class MyClass {
let button = UIButton()
#objc public func parentFunc(_ sender : UIButton?)
{
func childFunc() {
print("User tapped")
}
if sender != nil && sender.tag == 100{
childFunc()
return
}
button.addTarget(self, action: #selector(parentFunc(_:)), for: .touchUpInside)
button.tag = 100
}
}
In above code, Sender is optional so you can pass nil when you don't want to call child function like parentFunc(nil)
Is there any way to perform childFunc with #selector
No. The entire idea makes no sense. Keep in mind that the nested (inner) func does not really exist. It only comes into existence, momentarily, when the outer func runs. The running of the outer func brings the inner func into existence at the moment the definition is encountered, effectively storing it in a local variable, so that it is callable from subsequent code in scope inside the func. And like any local variable, when the outer func code ends, the inner func goes back out of existence. Also, because it is a local variable inside the func, it is scoped in such a way that it couldn't be called into from outside, even if such a notion made sense — just as a local variable inside a func cannot be seen from outside the func, even if that notion made sense.
The selector must point to a method of a class instance (where the class derives from NSObject). This is a Cocoa Objective-C architecture; you have to obey Objective-C rules.

Resources