IOS 9 - UIButton on subview does not fire IBAction outlet - ios

I've been struggling with this for a couple of days now and haven't been able to pinpoint the cause of my problem, as the title says, a UIButton on a subview is not firing the IBAction outlet when "clicked".
Let me elaborate, I'm working on a "tinder-like" app, where I'm using this third party library. I'm implementing customized views which have buttons and other controllers in them. The buttons in these "Cards" are not firing their IBAction outlets.
As far as I can tell, the library does the following:
Stacks 3 "DraggableViews" on top of each other, each draggable view has 2 child views, one is the content view, where my custom view lives, and the other is an overlay view, which has an image view on top. The draggable views use two gesture recognizer to do its thing, the first one is a tap gesture that calls a delegate method to handle tap events in the card (in their example its used to show a browser view). The second gesture is a pan gesture used to implement the swipe functionality of the card.
I've done my homework and have tried a few different solutions but I can't get the IBAction to fire. I've tried the following:
Use the parent view of the DraggableViews to disable the gesture recognizer when the touch event is triggered on a button. - This works (gesture is disabled) but the IBAction is not fired. on the case of the pan gesture, I am no longer able to "swipe" the card when touching the button and the tap event does not hit a breakpoint in the delegate method mentioned above. In the UI, my button reacts to the touch as it animates, but its outlet in the view controller fails to hit a breakpoint. I've disabled this as so:
//- Called when each DraggableView is visible (not called for view at index 0, but works alright for debugging in the mean time)
func koloda(koloda: KolodaView, didShowCardAtIndex index: UInt) {
let subviews = koloda.subviews
for subview in subviews {
if let recognizers = subview.gestureRecognizers {
for gesture in recognizers {
if gesture is UIPanGestureRecognizer || gesture is UITapGestureRecognizer{
//- Also tried setting this to false but nothing.
gesture.cancelsTouchesInView = false
gesture.delegate = self
}
}
}
}
//- On the gestureRecognizerDelegate
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if touch.view is UIButton {
return false
} else {
return true
}
}
Implemented a tap gesture on the button programmatically, On viewDidLoad I add the action to the tap gesture but this does not trigger the outlet either.
Programmatically set the userInteractionEnabled property of the ImageView to True hoping that this will allow the touch event to go through the responder chain (although I read in another post that this would have the opposite effect?).
I've also checked in the storyboard that all the relevant views and controllers in my custom view have the userInteractionEnabled option enabled.
I don't know if its relevant, but my custom view lives in a xib file, when I pass the view to the library I do it by instantiating the view controller and passing passing over its view, as so:
if let vc = spiViewConntroller {
return vc.view
}
Any help or suggestions would be greatly appreciated!
Cheers!
EDIT:
Continuing with my search for truth, I've completely removed the overlay view from the library, inspecting my custom views on the UI debugger i can see that the overlay view and its ImageView are no longer there. The button still does not fire its outlet so I can assume that the overlay view is not causing this issue.
I've forked the Koloda library and created a branch with a demo example, the branch name is "StackOverflowDemo". I've added a custom view with one button, I've created two outlets in its view controller where I'm changing the title of the button (which works) on view did load. I've also disabled the two gestures on the button to replicate what i've currently got in my app. If you do clone this down you'll need to swipe the first card off as the card at index 0 wont have the gestures disabled.
I'll keep digging, hopefully someone can pinpoint what I'm doing wrong!
Cheers.

Daniel.
Your issue is that you don't add your view controller as child to view controller which operates with Koloda. In the result your vc.view is shown, because Koloda retains it, but nobody retains your view controller, so you are losing important lifecycle methods and it gets deallocated.
The approach your trying to use is called Container View Controller. Apple has suggestion about its implementation here: https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html
Simple implementation here:
- (void) displayContentController: (UIViewController*) content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}

OK, I found my answer. After digging some more bit i was able to solve this. However my solution feels dirty and I'm unsure if it breaks the MVC pattern.
I think my issue is that my custom View Controller for my xib file is somehow lost or unable to respond to these events. When creating the views to be displayed as the content of the draggable card, I was creating an instance of my view controller and returning its view. What's weird to me is that the ViewDidLoad method of the view controller was doing its job when the view was loaded (i.e. changing values of outlets).
Solution:
I removed the file owner from the lib, created a custom class that inherits from UIView, say MyCustomUIView and moving the view controller logic, its outlets and actions to that class. Then on the xib file, I linked the content view to MyCustomUIView. However this solution feels dirty as I'd expect the view's view controller to handle all the logic. Might need to do a bit more reading on nib and reusable views.

I can suggest a way, that can help you to detect the problem. Use the debugger view in xcode when the simulator is running. It will show you the hierarchy of views in 3-D mode, which can help you in finding out the issue.
How to use debuggin view:
https://developer.apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html

Related

iOS UIButton in programmatically instantiated subview not firing

I have a Nib in my storyboard which is not connected to anything but I'm instantiating it as a subview thusly:
-(void)LoadCameraOverlayView
{
CameraOverlayViewController *cameraVC = [self.storyboard instantiateViewControllerWithIdentifier:#"CameraOverlayNib"];
cameraVC.view.userInteractionEnabled = YES;
[self.view addSubview:cameraVC.view];
}
The UIViewController has a camera with feedback which is working fine and a button which (when I segue to the view controller also works fine.)
The button is linked to a Touch Down event and Touch Up Inside event.
When I click the button I can see it is changing visually from its default state to its focused or highlighted state however none of the code seems to be executing. If I put an NSLog in the ViewDidLoad I can see that in the console but not for the linked method. However if I segue to the view it does work.
What's going on?
I looked at other solutions but I don't think it could be that there is a view in the way or that the CGRect isn't calibrated correctly or anything since I can see the visual change when clicking the button.
This code violates Rule One of how to use view controllers:
CameraOverlayViewController *cameraVC = [self.storyboard instantiateViewControllerWithIdentifier:#"CameraOverlayNib"];
[self.view addSubview:cameraVC.view];
You cannot simply instantiate a view controller and then add its view manually to the view hierarchy like that. There are very strict rules about this; there is an elaborate "dance" that you must do in order to add a view controller's view to your interface manually, and you are not doing the dance.
The result is that your cameraVC comes into existence, is given no place in the view controller hierarchy, and vanishes in a puff of smoke. There is thus no object for your button to talk to, and so when the button is tapped, nothing happens.
My suggestion would be: use a .xib file, not a storyboard scene, for this view; and after you have loaded it and put it into the interface, use code to configure the button so that it talks to self, your current view controller.

How do I make inputAccessoryView stay on screen when using drag to dismiss?

I know that the view controller must be firstResponder in order for the inputAccessory to stay at the bottom. I am using a custom inputView / keyboard. I can manually dismiss it with a done button by removing the inputView but not resigning first responder. However when I enable the interactive drag to dismiss on my scrollview, the code automatically resigns first responder. So how can I use the interactive drag to dismiss and yet keep my viewcontroller as first responder? Anyone done this before? I thought maybe it is not possible and that I may need to make my own interactive drag to dismiss using a gesture recognizer.
More info:
I have a button that swaps between standard keyboard and my custom one. I have seen dismissing these cause 2 keyboard did dismiss notifications. I thought I could become firstResponder in the keyboardDidHide method but this didn't work well since I couldn't tell the difference between when I manually dismissed the keyboard and when the interactive drag does it. This matters because I don't need to reload the input view or become first responder when I manually dismiss because I took care of it already.
Any suggestions would be amazing. I am trying to use inputView and inputAccessoryView on the UIViewController level.
Well after a day of pulling my hair, I have an answer.
Using the canResignFirstResponder of my viewcontroller did the trick. In viewWillAppear I set a BOOL responderOverride = YES;
In viewWillDisappear I call
responderOverride = NO;
[self resignFirstResponder];
When the interactive drag on the scrollview tries to resignFirstResponder, canResignFirstResponder returns no which prevents my viewcontroller from resigning and keeps my input accessory retained and sitting at the bottom of the screen.
There is a lot of other code with reloading input views but since the real question was how to force a controller to stay first responder so we don't lose our input accessory view, then this solution works.
override var canBecomeFirstResponder : Bool {
get {
retrun true
}
}
This works for me

Full-window view in front of a collection view

I'm trying to implement the following behavior:
Long press on a collection view brings a full-window view (call it LetterView) to the front
Subsequent gestures/touches are only processed by the LetterView.
(edit: I should mention that I want a transparency effect of seeing the collectionview items beneath LetterView)
I seem to be running into behavior that everyone else is trying to implement, though - my touches get processed by both the LetterView and the collection view. I.e. I can scroll the collection view AND have hits processed by my topmost view. Showing the view hierarchy in XCode clearly shows LetterView at the front, and both the UICollectionView and the LetterView are subviews of UICollectionWrapperView.
LetterView is a UIView subclass with a UIViewController subclass. It's added to the view hierarchy programmatically, inside my UICollectionViewController subclasses's viewDidLoad method, like so:
super.viewDidLoad()
letterDrawingViewController = LetterDrawingViewController()
let viewFrame : CGRect = self.collectionView!.frame
letterDrawingViewController.view = LetterDrawingView.init(frame:viewFrame)
letterDrawingView = letterDrawingViewController.view
self.addChildViewController(letterDrawingViewController)
letterDrawingViewController.didMoveToParentViewController(self)
collectionView?.addSubview(letterDrawingView)
It doesn't appear to be a first responder issue, as I tried overriding canBecomeFirstResponder in LetterView and assigning it first responder status when I move it to the front
I tried setting userInteractionEnabled=FALSE on the CollectionView, but keeping it true on the LetterView after I moved LetterView to the front. This disabled all touch events for both views
I tried setting exclusiveTouch=True for LetterView when I moved it to the front. This didn't appear to do anything.
Aside from any specific tips, are there any general techniques for debugging hit-testing like this? According to the docs on hit-testing in iOS, iOS should prefer the "deepest" subview that returns yes for hitTest:withEvent:, which, since LetterView is a subview of collectionview, and in front of all it's cells, should be the front? Is there any logging I can enable to see a hit test over the view hierarchy in action?
Thank you!
Nate.
If letterView is full screen, you probably don't want to add it as a subview of the collection view like you are. Maybe try adding it to the application's window instead and see how that does. At least in that instance it should intercept all the touch events.
Another method, although admittedly a more fragile feeling one, would be to enable and disable user interaction on the collectionView as you present and dismiss letterView.
So, when letterView is about to be presented, you can call
self.collectionView.userInteractionEnabled = NO;
and if you also know when that view is about to be dismissed you can call
self.collectionView.userInteractionEnabled = YES;
The only thing here to worry about is that you don't get into a bad state where your letterView is not presenting and your collectionView is also ignoring a user's touch. That will feel totally broken.
Whilst I think you can deal with your issue somewhat easily I think you are making a design mistake. It feels like you are trying to code this thinking like a web developper by adding a child view to your view and trying to intercept the touches there like one would do in a modern JavaScript single page app. In iOS I think this is bad design. You should segue or present the new viewController using the methods provided by apple.
So your code should look soothing like:
letterDrawingViewController = LetterDrawingViewController()
self.presentViewController(letterDrawingViewController, animated: true, completion: nil)
iOS8 has the added benefit of allowing you to have awesome custom transitions. Take a look at this : http://www.appcoda.com/custom-segue-animations/

touchesBegan delay after presenting UIView

I'm trying to load a UIView and then right away detect touches on that new view. Currently, overriding touchesBegan gives a delay of around a second.
So, I load up the UIView and immediately keep tapping on the screen. It takes around a second for touchesBegan to be called. From that point on, all is well. However, I can't afford the ~second wait initially.
I have stripped back all the code in the UIView to just the barebones incase anything was clogging up the main thread, but the delay still persists.
How can I go about getting immediate feedback of touches from a newly presented UIView? Thanks.
-- EDIT BELOW --
I've been playing around with this for the past few hours. Even when creating a custom UIWindow and overriding sendEvent, the UITouchPhase gets halted when the new view is displayed. To begin receiving events again I have to take my finger off the screen and place it back on the screen. (I don't want to have to do this).
The problem seems to lie with the segue to the new view controller. When it segues, the touch phase is ended. If I simply add a subview to the current view controller, I see the desired functionality (i.e. instant responding to touch).
Given my newly presented view contains a lot of logic, I wanted to wrap it all up in it's own view controller rather than add it to the presenter view controller. Is there a way for me to do this and use 'addSubview` to present it? This should hopefully achieve the desired effect.
In the end, I created a custom view controller with it's own xib. Where I would have segued, I now instantiate that custom view controller and append it's view. This has eliminated the touch lag.
Have you disabled multi-touch? There's an inherent delay while the controller waits to see if there's a follow up touch (on all single touches). The initial sluggishness might be from loading up the multi-touch code and deciding what to do about it.
myViewController.view.multipleTouchEnabled=NO;
As to your final question, look into view controller containment. Since iOS 5 Apple has provided the hooks officially and safely to present one view controller as a sub view of another.
Sadly I've no insight as to the greater issue.
I found an answer that worked for me from a similar question asked here:
iOS: Why touchesBegan has some delay in some specific area in UIView
This solution isn't check-marked on that thread, so I'll copy it here to make it easier to find.
override func viewDidAppear(animated: Bool) {
let window = view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}

iOS programming: customized buttons in viewDidAppear

I created regular buttons in .xib file and I added a gradient effect to them and shadows in the code in this section:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
... my customized buttons code here
}
When I modally switch to another view controller and then go back to the original one the xib file gets redrawn but all the gradient effects and shadows disappear. Any ideas?
I'm not sure entirely what's going on, but note that after you dismiss the modal view controller, viewDidAppear: gets called again. If you only want to make these buttons once, you could move your custom button code to viewDidLoad.
I haven't experienced this problem before to know what's going on, so it would be helpful to see the code itself. But I suspect that moving the code to viewDidLoad would solve the problem.
When modal View is dismissed, ViewDidLoad is not called, But ViewWillAppear and ViewDidAppear is called, But you say that the code is written in ViewDidAppear, and still it doesnt work. I suggest you to write that code in ViewWillAppear and check.

Resources