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

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.

Related

iOS UITest - Navigate to all available screens

I am using iOS UITest for a Swift application. I use something like,
func testAllScreenNavigation() {
let app = XCUIApplication()
app.tabBars.buttons["Home"].tap()
app.navigationBars["Home"].buttons["More"].tap()
app.sheets.buttons["Cancel"].tap()
}
etc. to navigate some of the specific, tabs, buttons, etc. and switch to respective screens. But i want to navigate each and every screens of my Application (It can be BFS style navigation or DFS style navigation, no matter). Is there any way iOS provides so i can get all navigable elements and then explore deeper and deeper automatically for my App?
I also need to keep trace of which xcuoelement in a screen is already processed and which are not yet processed.
The only way I can think of is using Xcode UI test recorder feature.
While you are recording, navigate through all of your screens via the device/simulator and then the XCUIApplication() variable would be recorded with the appropriate references.
If the button/nav bar/any element has text on it, it will show up in the recorded code or else it will be referenced numerically.
Hope that helps.
Kind regards,
Mukund
I like your idea for getting all views and check whether the layouting and localization for example is fine.
I think you need to specify your criteria for "screens" and how they are accessed.
Basically, one could thing of the following structure
- UITabBarController
-- UISplitViewController
--- UINavigationController
---- UIViewController
----- UIBarButtonItems
----- UIView
----- UIButton
----- UISwitch
----- UITableViewCell
You could now go top down from the UITabBarController to the next controlling instance (might also skip one, e.g. SplitViewControllers on iPhones).
You can use the general property:
XCUIApplication().tabBars
Nevertheless that transition is the problem: How would you get from one ViewController to another and are they all position in the ViewController's View or do you have to loop the subviews of a view.
UIButton -> Touch Up Inside
UISwitch -> Value Changed
UITableViewCell -> DidSelectRowAtIndexPath
UIView -> UILongPressGestureRecognizer
This is how I would basically set it up:
For each UIViewController instance, get the related View (and perform the following call recursively).
Check all the subviews of a view.
For UIViews, go even further and check their subviews
For UIButtons, perform TouchUpInside
and so on.
Make sure to have a condition to stop going deeper, as UITableViews got a lot of subviews or your UIWebViews would of course be set up in a different way.
This way you should be able to navigate through a lot Views in your app hierarchy, but you will need some extensions for UIBarButtonItems, custom Gesture Recognizers and of course also for your "special" controls that might listen to value changes and perform a layout-change.
Accessing specific elements
In addition to the above approach where you simply get an array of elements of a specific type, you can access specific elements (e.g. those where you know they are of a very specific type with certain ValueChangeListeners or something)
To access a specific object in particular, like the TabBar example from above, you can use the accessibilityLabel like so. At first you need to declare the accessibilityLabel in your code or in the .xib-file/.storyboard:
// just to illustrate, so you get an idea:
self.tabBarController.isAccessibilityElement = true
self.tabBarController.accessibilityLabel = "tabBar"
And then do:
let tabBar = XCUIApplication().tabBars["tabBar"]
Here is Apple's documentation for setting these accessibilityLabels:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html
A great way to get the related identifier of an element would be to use the Accessibility Inspector from Apple:
https://developer.apple.com/library/content/technotes/TestingAccessibilityOfiOSApps/TestAccessibilityiniOSSimulatorwithAccessibilityInspector/TestAccessibilityiniOSSimulatorwithAccessibilityInspector.html
Accessing elements in general
To access elements in general, you need to make use of the XCUIElementType of these objects, here you will access the objects based on their classes.
E.g. you could call:
"tabBars", "navBars", "tables", "buttons", and so on from the elements in general.
Still you would be facing the issue with "special controls". As the Apple documentation lacks (imho) some detail about properties and attributes, I do recommend the docs here: https://blog.metova.com/guide-xcode-ui-test/ It provides a great overview of what is accessible and may help you getting some better understanding.
An overview of the available XCUIElementTypes can be found here. Basically, the elementType property is an enumerated value that represents the type of an element. XCUIElementType is a very large enumeration and some of its members do not apply to iOS applications (they apply to MacOS X apps). Some of the more commonly used values are:
Alert
Button
NavigationBar
TabBar
ToolBar
ActivityIndicator
SegmentedControl
Picker
Image
StaticText
TextField
DatePicker
TextView
WebView
https://developer.apple.com/reference/xctest/xcuielementtype?language=objc

How to set up #IBAction to target a UIView subclass

I'm trying a storyboard-free approach, and attempted the following:
I created a FooView.xib to hold my layout. It has a few buttons and labels.
I created a FooView.swift to hold a FooView subclass of UIView. At the moment, it doesn't really do anything.
I created FooViewController.swift to manage my FooView instances. I call it to create FooViews as follows:
init() {
super.init(nibName: "FooView", bundle: nil)
}
I set the File Owner of FooView.xib to the FooView class
I set the custom class of the FooView view as FooView
I dragged some #IBAction outputs from the IB to the FooView implementation
When my actions fire, I get a runtime exception: "unrecognized selector sent to instance" that references the FooViewController(!). I double checked the File Owner and all the outputs, all references look correct.
It acts like it ignores the File Owner with respect to the event dispatch.
(It feels like I'm fighting the framework and really am expected to implement all my outputs/actions inside the controller, but from an architectural purity perspective (to enhance testing) I was leaning towards hiding the xib control details and making a more opaque view that didn't directly expose its inner controls to the controller, but instead had a protocol/delegate kind of contract.)
I have a sneaky suspicion that if I moved all the xib-loading logic into the UIView implementation that things would work, but that feels like a larger adventure. Is there an easy way to set up my FooView subclass to receive the events?
Another thing you could use is set the First Responder as the target for your action. Although you won't get the nice neat indicator in XCode that you IBAction is connected, it will pass the message to the first class in the responder chain that implements that method.
To set it up in interface builder, click on First Responder and open the Attributes inspector. Add your method to the User Defined list as someAction: (don't forget the colon). Then you can connect your button to the first responder and that method will be an available option for you to connect it.
Lastly you declare your IBAction just like normal in the view class you want to implement it.

How to access custom view properties after initialize

I'm targeting IOS 8+.
I have a form that is used in more than one place. So I decided to create a custom view where I define the various "form" text fields.
I have built my XIB, and the UIView subclass contains the outlets for each textField.
The view is composed of a background image and a scroll with the form fields over it.
Now, my first obstacle was: I need to have this custom view in a container that may or may not have a navigation bar. This made me create a constraint outlet so I could update its value to push down the scroller view. This way I'd have the whole image in the frame, the top being behind the navbar and the scroller bellow the nav bar).
Here's a manual drawing to help understanding the problem.
It's very possible that I'm making a lot of mess and confusion on my way to solve this. :)
The problem is:
After awakeFromNib runs I have no access to the constraint property. I then noticed the same thing happens for the TextFields outlets.
So, how can I access the custom view's properties when I instantiate them programatically?
Something like:
Controller:
let customView = SignupView(frame: f)
view.addSubview(customView)
customView.pushScrollerDownBy(50.0)
Custom view:
func pushScrollerDownBy(yOffset: CGFloat) {
//topScrollerConstraint is the outlet for the textField.
topScrollerConstraint.constant = yOffset //right now topScrollerConstraint is nil.
}
You should check if you have connected your topScrollerConstraint to the file's owner since it will not get instantiated and therefore, error. Here is a recent SO question regarding difference between these two:
What is File’s owner in XIB in this case?

Can't link a bar button to an #IBAction in superclass

I have a number of view controllers, each with their own menu button (a UIBarButton, added in the storyboard). Now I want to link all these up to a single #IBAction function in their superclass (the superclass is the same for all the view controllers with that menubutton).
Now I have linked up #IBOutlets to a superclass before, but it doesn't seem to work with #IBActions, even though the function isn't private, and it definitely is part of the superclass (I am refactoring, previously it was an #IBAction in each class, which only did menuButtonTap() (calling the method in the superclass).
Any ideas?
I have solved the problem by manually creating an #IBAction on the superclass, and giving them the same name as the ones I create in the subclasses. Then I deleted the ones in the subclasses. This leaves a 'dangling reference' from the storyboard, according to Xcode, but I know it's there.
Although this still does not work as of Xcode 9.4 for general purpose UIViewController (but your workaround still does work 👍🏻), please note that it works as expected for UITableViewCell templates in storyboard.
If some of your template cells in storyboard share the same base class containing #IBOutlet properties, you will be able to link them to every template cell instance as you usually do:
Then Xcode will show a popup for telling in which prototype cell the link is "backed":
I'm not sure why this second step is necessary though, since you designate a specific component from within a given prototype cell
You can do it like you do when adding an action to a UITabbar button from subview class.
Assuming btn is a UIBarButtonItem,
[btn setTarget:self.superview];
[btn setAction:#selector(menuButtonTap:)]
Are you using __unused keyword by any chance? If you do the IBAction won't show up in storyboard(I am using Xcode 6.3.2)
- (IBAction)actionBack:(__unused id)sender;
vs
- (IBAction)actionBack:(id)sender;
To make it show up and selectable removed the __unsused keyword.

Determining a visible part (frame) of UIView subclass, that is added as a subview to cell in table view

Let's call this UIView subclass - SomeClass. This SomeClass is a part of a static library. Some customer will use this library and will add instances of this SomeClass to the cells of his (customer's) table view.
I (SomeClass) need to determine when the SomeClass "enters" screen (will become visible), and when will "exit" screen (will become non-visible).
I can use didMoveToWindow: method and then check self.window for nil. BUT, there is a problem, SomeClass gets this event, before it is actually visible, because of cells "preparation" by table view concept. And I need to know for sure, it is 100% visible by some user.
One way to determine is by using scrollViewDidScroll:. Suppose SomeClass will get scroll view by using iteration on super views and will subscribe as a delegate to found scroll view. But he will be removed by some cell that will subscribe itself as a delegate to scroll view. So I need to invent here some solution for this. For example, in Android, there is possibility to add observer, in that case SomeClass is always a listener and is not overriding any other listener. There is many to one relation in Android, not like in iOS, one to one.
The other way, I can enable some timer in didMoveToWindow: when SomeClass becomes visible, that will check each X time, its frame. The timer will be disabled, when SomeClass will go from screen.
Probably there is a way to check at low level, without using scroll view and timer on some low-level redraw method. Is it possible?
So what is the best (will use less resources / good design) method?
You can use CGRectIntersectsRect to check if the cell's frame intersects with the frame of your custom view. Aside from that, didMoveToWindow is the method you are looking for.
If as you say the table view cell will always have SomeClass as a subview, then it would make more sense to use UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:.

Resources