Button Action using Reactive Cocoa - ios

can anyone say how do i observe for action in Reactive Cocoa for a UIButton or UIControl..
An alternative way is to bind the view to the view model.And observe changes on the Mutable Property.
I tried using below code but none is firing.
self.rollBtn.reactive.trigger(for: .touchUpInside).observeValues {
value in
print(value)
}
EDIT: Actually i am trying to get the sender on button Tap..how can i do that?

You have done nothing wrong in this code snippet - trigger(for:) is one of the ways to get notified in RAC 5.0. It should print () for every press on the button.
Have you linked the button with the view, if you are using Storyboard or Interface Builder? Where did you place this piece of code? Make sure you place it in viewDidLoad or awakeFromNib so that it gets called before the view is presented.
-
EDIT: Actually i am trying to get the sender on button Tap..how can i do that?
As mentioned in the comments, trigger(for:) returns a Signal<(), NoError>. It doesn't include the sender with the value event. You would need to reference the sender manually, e.g.:
button.reactive
.trigger(for: .touchUpInside)
.observeValues { [unowned button] in
_ = button
}

Related

UIControl Not Calling Event Handlers If UITextFields In SuperViews Are Active Responders

UIButton
I have a View Controller in which the user needs to enter in mission level data. The view controller then has a custom UIView (Crew View) in its view hierarchy where there is a button that the user can press to add Persons to the mission. Those persons are custom UIViews as well (Person Views) that get added as children to the Crew View.
The layout looks something like this:
VC
|_ScrollView
|EntryFields
|_ Crew View (UIView subclass)
|UIButton
|_ Person View (UIView subclass)
|EntryFields
|UIButton
|_ Person View
|EntryFields
|UIButton
...
When there is no first responder for the keyboard, all the buttons pressed trigger their respective handlers regardless of where they are in their View Hierarchy.
Now if an entry field in the PersonView is active I'm able to use both the UIButton in the PersonView as well as the UIButton higher up in the Crew View.
But if an entry field in the top level is active, the handler for the buttons are not called.
Note: I know the buttons are receiving the touch events because their UI changes since the .highlighted state gets activated. The button handlers are set to .touchUpInside.
UISwitch
This has also been an issue in another part of my app with a UISwitch. The view hierarchy looks like this:
VC
|_ScrollView
|Entry Fields
|_Some UIView Subclass
|UISwitch
When the keyboard was active in the scenario (from the Entry Fields up the view hierarchy), the UISwitch would not call it's handler (set on .valueChanged).
My Solution: I removed the target handler from the UISwitch and instead set a tap gesture recognizer for the whole view that would manually trigger the switch, this solution worked regardless if there was an active first responder up the view hierarchy.
Has anybody experienced this before? Why are the event target handlers not being called on the UIControls(yet they're still responding in the UI) when there are active first responders in their superview but gesture recognizers work just fine.
Everything is built programmatically, not that it should matter and the View Hierarchy debugger on XCode shows that the UIControls are not being blocked.
More Information
So the EntryFields is actually a custom component (to follow material design with a floating placeholder). The view hierarchy of the EntryField is as follows:
EntryField (UIView subclass)
|_ StackView
|_ UILabel
|_ UITextField
Everything in this project is done programmatically with auto layout constraints. The EntryField interacts with its delegates by forwarding the protocol methods from the UITextField. I don't know if this is messing with the first responder chain in the application.
Here's a sample project (gutted version of the production code) that replicates it perfectly.
https://github.com/barbulescualex/55051678
I've checked your code and found issue in your button declaration. you need to use lazy in order to get the events.
i.e.
lazy var addButton : UIButton = {
let button = UIButton()
button.setTitle("Add Person", for: .normal)
button.setTitleColor(UIColor.purple, for: .normal)
button.setTitleColor(UIColor.green, for: .highlighted)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(addPersonField(_:)), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = true
return button
}()
output
https://drive.google.com/open?id=1_u4a3anvOPuZGlJ3VtM15D6aBdXsclMm
self: The self keyword refers to the current class instance, from
within that class instance.
func addTarget(_ target: Any?,
action: Selector,
for controlEvents: UIControl.Event)
Target: the object whose action method is called.
Why would self in the closure work and refer to my view controller if clicked on normally vs
it not working if there's an active textfield in the superview:
From the docs
The control does not retain the object in the target parameter. It is
your responsibility to maintain a strong reference to the target
object while it is attached to a control.
i.e. It depends upon the current state of instance (Life Cycle) and how the iOS handle it.
When we use let myButton: UIButton = {...}() you're immediately assigning value to myButton variable there are chances that 'self' hasn't been initialized by the OS
.In order to make sure when we addTarget to our button object, 'self' is initialized properly we use lazy keyword
Lazy: Lazy initialization (also sometimes called lazy instantiation, or lazy loading) is a technique for delaying the creation of an object or some other expensive process until it’s needed.
When programming for iOS, this is helpful to make sure you utilize only the memory you need when you need it.
With lazy we are making sure that our control will have a strong reference to current class which is going to handle the action
I am not sure I fully understand the issue you're experiencing, so I did a little demo that should reproduce yours, even though I am not experiencing the same behaviour as you do, at least not for now.
Check it out (video) and let's see if it's on you or not.
Now, I have the following hierarchy in my demo project:
And when I run it it looks like this:
Color legend:
RED: VC's View (self.view)
BLUE: ScrollView
GREEN: Crew View
I have no issues at all as you can see in the video, what's different from yours?
LATER EDIT:
Ok, the issue is more than meets the eye, the good news is that there's no issue, this is just how iOS works and given this opportunity I am going to explain as detailed as possible what really happens.
Firstly, your question was why it works with the bottom textFields but it doesn't work with the top ones.
ANSWER:
You are messing it when you declare the addButton as a let instead of a lazy var, then you add as a target self, which in that scope is the closure itself and after you exit the scope, if you place a breakpoint in func addPersonField you should see that sender.allTargets has no targets. That's ok with the compiler since you can, there was a time when you couldn't do it, but now you can because target is declared as Any?, which means you could even set nil as a target and you'll experience the same behaviour.
Now, you might wonder why it works with self as a closure which gets deallocated after you exit the scope, or why it works just as fine with nil. The doc for addTarget(_:action:for:)
says that:
If you specify nil, UIKit searches the responder chain for an object
that responds to the specified action message and delivers the message
to that object.
Which is your PersonsView, which doesn't get deallocated since is always on screen and has the specified action your func addPersonField. That's why it works before you start using any textfields (I know it works with the bottom ones, I'll get there).
Why it works with the bottom textFields you'll wonder, right? Well, again... you're doing the magic here without knowing, if you tap on a bottom textField that object becomesFirstResponder, now when an event occurs (like your .touchUpInside on the Add Person button) if the firstResponder can't handle it, UIKit sends the event to the text field’s parent UIView object, which in this case is the stackView, if the stackView can't handle it, the event is sent to the stackView's parent UIView which is exactly the PersonsView - which is the golden one, because it responds to the selector you specified addPersonField(_:).
See the personsView below:
On the other hand, when you tap on the TOP textFields (those EntryFields), they are embedded by a horizontal stackView which is embedded by a vertical stackView, which is contained by the ScrollView. Now, if you followed me til here, you got the idea, the Responder Chain goes from EntryField -> Horizontal StackView -> Vertical StackView -> ScrollView -> etc but it doesn't look in the other stackViews that contain your PersonsView, where you defined the selector, that's why it doesn't work here.
See how the stacks are on the same level, embedded in the same VerticalStackView which is embedded in the ScrollView below:
Even though, if you tap on a Top EntryField/TextField, and press return on the keyboard, you call view.textField.resignFirstResponder() which enables the Add Person button again.

What's the difference between AnyObject and UIbutton as sender?

When defining an IBAction, there's an option for Anyobject and UIButton, both works, what's the difference?
Yes, both works. The difference is just that by declaring it to a button, you get a typecasted reference as UIButton instead of AnyObject (or id in Objective-C). If you did not do that you will have to do it manually inside the code.
You should prefer leaving it to AnyObject in case the action has some code you would like to call from anywhere else, rather than just the button action.
For example a refresh button, you might have to do a refresh programatically. If you have set your action parameter to UIButton, you have to send an UIButton (but why??? You are doing it programatically, right?). Whereas if it is AnyObject, you can send 'self'. Which will also make you distinguish whether the refresh was done on action or programatically.
Hope that explains.
Sorry for no code. I am not into Swift coding much. Edits are welcome.
EDIT
So, even if it is AnyObject, it does have your UIButton properties. Just type cast it to use them.
If you are only ever going to use the function with a UIButton it is best practice to declare your sender as a UIButton. This saves you a bit of code and it also tells anyone in the future reading your code that you only expect the function to be used with a UIButton.
Using AnyObject or Any will work, but you will need to do an guard let button = sender as? UIButton { return } in order to access it as a button. This way allows you to react differently depending on what the sender actually is, but I don't recommend doing that.
If it's anyObject, then it doesn't have any of UIButton's properties. When you click the button and the IBAction fires, sender contains information about the thing that triggered the action.
For example, for a UIButton, you might want to query the UIButton's text when the IBAction triggers.
However, in a situation where the IBAction is connected to two different UI controls, let's say, a button and a slider, querying the sender when it's of type UIButton (while the triggering UI element is the UISlider) will crash the program. If you have AnyObject, you'll be able to test if the sender is a UIButton or a UISlider, and then do something.
In general, if you don't care about sender, leave it blank, so people reading your code will know that you aren't using sender for anything.

Trigger button inside UITextView Event

Im trying to trigger a button event inside the UITextView on change event
I tried
-(IBAction)myTextfield:(id)sender
{
[self.myButton sendActionsForControlEvents:UIControlEventTouchUpInside];
}
when NSLog(#""); the event it will show up in the debugger but it does'nt do anything with the sendAction
Does someone know what im missing or doing wrong
Thank you very much.
Indeed i created a -(void)myMethod; and put my code in the this method
-(IBAction)myTextfield:(id)sender
{
[self myMethod];
}
You must register the action to your button. see the home documents:
UIControl implements this method to send all action messages associated with controlEvents, repeatedly invoking sendAction:to:forEvent: in the process. The list of targets and actions it looks up is constructed from prior invocations of addTarget:action:forControlEvents:.
so you use the addTarget:action:forControlEvents:. to your button ,so it works.

What's the difference with :(id)sender?

What's the difference between declaring a UIButton in Xcode like this:
- (IBAction)testButton;
and declaring a button like this:
- (IBAction)testButton:(id)sender;
I understand that in the .m file you would then implement the buttons accordingly, as shown below:
- (IBAction)testButton
{
// insert code here..
}
and setting it up like this:
- (IBAction)testButton:(id)sender
{
// insert code here..
}
Is there any additional things you can do by declaring the button with :(id)sender, is there some additional stability, or is there no difference?
With :(id)sender you are able to access the button itself through the sender variable. This is handy in many situations. For example, you can have many buttons and give each a tag. Then use the [sender tag] method to find which button was tapped if many buttons are using this IBAction.
- (IBAction)someMethod:(id)sender {
// do stuff
}
Using (id)sender, you have a reference to who sent the method call. Please note, this doesn't have to be limited to a UIButton.
If you're created this method via control-dragging from the storyboard an only hooking up a single button, then sender is basically useless (it will always be the same), and should probably be marked as unused:
#pragma unused (sender)
(The compiler can better optimize your code if you do this.)
However, there's nothing wrong with hooking up several UI elements to the same IBAction method. You can then distinguish the sender via:
[sender tag]
...which returns an int that was either set via the storyboard or programmatically.
Moreover, you can call this method elsewhere in your class. You can either pass nil as the sender, or you can pass it a particular UI element in order to force it into the results you've coded for objects of that tag.
Nonetheless, if you plan to call the method with a nil argument, you can always throw:
if(!sender)
... into the method in order to handle special logic for when the method has been invoked programmatically as opposed to via user interaction.
It allows you to know which button you are working with. I have posted a simple example for a card game below
- (IBAction)flipCard:(id)sender {
[self.game flipCardAtIndex:[self.cardButtons indexOfObject:sender]];
self.flipCount++;
[self updateUI];
}
This method is used for a card flipping game. There are multiple buttons on the screen representing different cards. When you hit the button, a card in the model must be flipped. We know which one by finding the index of the variable sender

How to show the text of an UITextField in an UILabel?

I am building an interface, where I can add events like in a calendar.
In the AddAEventViewController I have Buttons to set the starttime, duration and recurrence.
Every time you press a button a viewcontroller comes up with a UIDatePicker, where you can set your time. The picked component is than displayed in a UITextField. Now when I press the Done-Button, it dismisses the ModalViewController and I am back to my AddAEventViewController. Next to the Durationbutton e.g. is a UILabel, where I want to show now the just picked and in the textfield shown duration.
How do I get access to the AddEventViewController out of an other ViewController? I tried to alloc and init a new one there, but it didnt work!
- (IBAction)pressedDoneButton:(id)sender {
_mainAddWishViewController.labelDuration.text=textFieldDuration.text;
[self dismissModalViewControllerAnimated:YES];
}
Can someone help me please!
Thank you Jules
There are several ways you can do this, all of them documented here. Reading and understanding them will help you a lot in iOS software development.
There are many ways to achieve this. Here is one that is fairly straightforward.
In the "child" viewController, add a delegate property and set it to the parent view controller.
Then in your Done button handler, do something like:
[self.delegate performSelector:#selector(didComplete) withObject:self]
In the parent view controller, define a method as follows:
- (void) didComplete: (YourSubViewControllerClass *) sender
{
self.labelDuration.text = sender.textFieldDuration.text
}
Basically, this implements an informal protocol whereby the subViewController informs the main view controller that it is finished and input values are available.
Note that if you cancel out of the subViewController, don't send the didComplete message.

Resources