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/
Related
Sometimes (but not all the time!) on iPhones (and iPhone simulators) I notice my UITableView header has this 'snapping' behavior that, when I try to drag down from the top of the screen it snaps back up instead of fluidly moving back up like a tableView normally behaves.
I'm wondering if anyone knows of this bug, what causes it, or how I can fix it? I feel like it might have something to do with UITableViewHeader but I'm not sure.
Unfortunately, I cannot share the code, but I don't believe it is something in the code. I manually commented on almost every line of the code and the problem persists!
Here was the problem for me and I'm pretty sure its a bug in XCode/Swift.
At first my navigation controller in the storyboard has this setting:
In my code I have the following method declared:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let hideBar = (viewController == self)
navigationController.setNavigationBarHidden(hideBar, animated: animated)
}
Used to disable the navigation bar on the home screen. The combination of these two (if the method is declared on the home screen) causes a bug as shown above. I am able to repeat this bug in a new project.
The solution to this bug is to check the Show Navigation Bar box on the Navigation Controller
Since code is not provided for this question, I am answering based on my assumption.
Assumption 1 - Using manual layout.
If you have code in layoutSubviews(). Then you might want to check your calculation again. Make sure view frames are calculated one time in layoutSubview() method.
Assumption 2 - View animation is blocked/delayed by some other tasks on main thread.
Make sure to run no-UI/API code in background thread.
Reuse cell instances: for specific type of cell you should have only one instance, no more.
Don’t bind data at cellForRowAtIndexPath: method ‘cause at this time cell is not displayed yet. Instead use tableView:willDisplayCell:forRowAtIndexPath: method in the delegate of UITableView.
Hey, can you move your header view content in first cell of first section, so that you can avoid header view problem. Then check if snappy problem is occurring.
I am trying to implement swipe up/down to dismiss view controller and everything is working great. If I start swiping up, it will finish the animation in the upwards direction and vice versa.
The problem appears when the user first swipes up but then decides to swipe the view controller down - how do I change the value in the animation to dismiss view controller to the bottom rather than to the top (set by the user's first swipe).
In my UIViewControllerAnimatedTransitioning controller I define the animation like this:
func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
UIView.animateWithDuration(duration, animations: {
guard let transitionDelegate = self.transitionDelegate else {return}
snapshot.frame.origin.y = transitionDelegate.shouldAnimateUp ? -snapshot.frame.height : snapshot.frame.height
TransitionDelegate points to UIPercentDrivenInteractiveTransition controller (where the swipe gesture is defined). ShouldAnimateUp is defined in UIPanGestureRecognizer function like this:
shouldAnimateUp = translatedView.center.y < translatedView.frame.height / 2
That is if the view is in upper half, shouldAnimateUp = true and the other way around.
But unfortunately, when I call finishInteractiveTransition() func it uses the value which was initially set in UIView.animateWithDuration in UIViewControllerAnimatedTransitioning controller when dismissViewControllerAnimated(true, completion: nil) was called.
So, is there any way to change values for animation in UIViewControllerAnimatedTransitioning controller after dismissViewControllerAnimated(true, completion: nil) is called?
PS: I kind of struggled to define my problem in words (words are hard 🙄 ), so please tell me if you need additional info or if I should try to rewrite my explanation. Also, an additional hint: I would like the animation to work like image dismissal works in official Twitter app.
OK, I see what they're doing. It's actually more complicated that "if you swipe down and then up, it animates scene off upward". If you swipe down and back up, the Twitter app will cancel if you don't get much past where you started, but will go up if you pass where you started by some considerable portion (and are still swiping in that upward).
I must confess that I'm not crazy about this UX, because two very similar gestures can result in very different behavior. If you swipe down, keep your finger down, and then flick back upward, there is a seemingly arbitrary nature as to whether it's interpreted as a cancelation of the downward swipe or as an upward swipe. I personally think that if the user initiates a demonstrable downward gesture, that dragging back up should merely be a cancelation of the transition. But that's not the question here.
Anyway, if this is what you want to do, there are a couple of ways to achieve it:
You could consider using view property animator-based animations, which handle mid-flight changes to the animation more elegantly than older animation techniques. So, you theoretically could just addAnimations to your UIViewPropertyAnimator. But it seems messy to me.
See Advances in UIKit Animations and Transitions for more information about view property animators and how to use them with interruptible custom transitions.
When dismissing, you might not use UIViewControllerAnimatedTransitioning at all. Just animate the dismissal yourself.
For example, when you present, the UIPresentationController subclass can keep the presenting view (by returning false from shouldRemovePresentersView). Then, when the gesture recognizer starts, it might not initiate a custom, interactive custom transition with dismiss at all, but instead merely adjust the frame of the presented scene (and modify the opacity of the dimming chrome). Then, at the end of the gesture, complete the animation manually and then the completion block can dismiss the presented view controller with no animation at all (because you've animated it yourself).
IMHO, this is architecturally inelegant. A view controller has no business mucking about with chrome that should be owned by the presentation controller. But, it works.
We should recognize that they might not have "presented" the full screen image at all. For example, they could have done simple view controller containment, but just added the child view controller scene to the existing scene. Then, the gesture could do whatever frame and dimming layer opacity changes it wanted, and when its done, just do the final animation and in the completion block, just remove the child view controller (e.g. willMove and removeFromParentViewController).
If I were to do this, I'd probably lean towards option 3, though I'd wager that it might not feel very satisfying for you, having invested time in custom interactive transitions already. Regardless, these are a couple of approaches you could consider.
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.
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
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
}