I am building a custom GUI on a universal app for iPhone and iPad. On iPad it relies heavily on "sideViews" for utilities like content manipulation, detailInformation and the like (think of an advanced SplitView). From a visual point of view the new UIPresentationController is spot on for letting me present these "sideViews" (and not using a dimmedView) and the implementation has been simple to build and maintain, while still integrate nicely with the storyboard. But I need to be able to manipulate the content of the presentingViewController while the presentedViewController is visible. So my question is, can I set userInteractionEnabled (or similar) on the presentingViewController while presenting the sideViews?
UIPresentationController
inserts its container view as a window subview above the presenting view, thus any touches outside the presented view get trapped by the container view and never make it to the presenting view.
The fix is to insert a view on the container view that passes through touches to the presenting view. You can use this as a dimming view or set its backgroundColor
to [UIColor clearColor]
for a fully transparent view. Set the passthrough views in your presentation controller code.
@interface IVPasserView : UIView
@property (strong, nonatomic) NSArray* passthroughViews;
@end
@implementation IVPasserView
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* hit = [super hitTest:point withEvent:event];
if (hit == self)
for (UIView* passthroughView in _passthroughViews)
{
hit = [passthroughView hitTest:[self convertPoint:point toView:passthroughView]
withEvent:event];
if (hit)
break;
}
return hit;
}
@end
Note: while this violates the spirit of -[UIView hitTest:withEvent:]
in that it doesn't return a subview, this is in fact how the system standard UIPopoverPresentationController
handles it. If you set the passthroughViews
property there, the container view responds to hitTest:withEvent:
with a passthrough view, even though they are not superview/subview! Thus it's likely to survive the next iOS release.
Modal presentation isn't suited well for your situation. It is better to implement your scenario with custom container view controller and override showDetailViewController:sender:
method to handle presentation of additional view controllers. You can adapt this method to show view controller modal on iPhone and on the right on iPad for example.
Here is an excerpt from Apple Documentation:
Presenting Versus Showing a View Controller
The UIViewController class offers two ways to display a view controller:
The showViewController:sender: and showDetailViewController:sender: methods offer the most adaptive and flexible way to display view controllers. These methods let the presenting view controller decide how best to handle the presentation. For example, a container view controller might incorporate the view controller as a child instead of presenting it modally. The default behavior presents the view controller modally. The presentViewController:animated:completion: method always displays the view controller modally. The view controller that calls this method might not ultimately handle the presentation but the presentation is always modal. This method adapts the presentation style for horizontally compact environments. The showViewController:sender: and showDetailViewController:sender: methods are the preferred way to initiate presentations. A view controller can call them without knowing anything about the rest of the view controller hierarchy or the current view controller’s position in that hierarchy. These methods also make it easier to reuse view controllers in different parts of your app without writing conditional code paths.
Okay, so it seems that the idea of the UIPresentationController is NOT to be able to use it as an advanced SplitView (or at least that's my current conclusion). I did managed to build a workaround though. If anyone finds a better way of handling this, please let me know in the comments.
So what I do is I insert PresentingViewController's view in the transitionContexts containerView (same as the UIPresentationControllers containerView) hierarchy at Index 0. This makes me able to transparently handle touchEvents in the PresentingViewControllers view. But it removes the PresentingViewControllers view from its original view hierarchy so I need to move it back there when the presentation is dismissed. It means putting the view back to the parentViewController's view if present, or the window of the app, if presentingViewController is the rootViewController of the app (there might be other scenarios too, but this will do for now).
This is all done in the animateTransition in the UIViewControllerAnimatedTransitioning.
Here's the piece of code:
UIView.animateWithDuration(transitionDuration(transitionContext),
delay: 0.0,
usingSpringWithDamping: 1.0,
initialSpringVelocity: 0.5,
options: UIViewAnimationOptions.BeginFromCurrentState|UIViewAnimationOptions.AllowUserInteraction,
animations: { () -> Void in
animatingView.frame = finalFrame
}) { (finished:Bool) -> Void in
if !self.isPresentation {
if let parentViewController = backgroundVC.parentViewController {
parentViewController.view.addSubview(backgroundVC.view)
}
else if let window = (UIApplication.sharedApplication().delegate as! AppDelegate).window {
window.addSubview(backgroundVC.view)
}
fromView.removeFromSuperview()
}
else {
containerView.insertSubview(backgroundVC.view, atIndex: 0)
}
transitionContext.completeTransition(true)
}
Adapting Glen Low's above great Objective C solution to Swift.
// View that supports forwarding hit test touches to the provided array of views
class TouchForwardView: UIView {
// Array of views that we want to forward touches to
var touchForwardTargetViews = [UIView]()
// Variable to quickly disable/enable touch forwarding functionality
var touchForwardingEnabled = true
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Get the hit test view
let hitTestView = super.hitTest(point, with: event)
// Make sure the hit test view is self and that touch forwarding is enabled
if hitTestView == self && touchForwardingEnabled {
// Iterate the hit test target views
for targetView in touchForwardTargetViews {
// Convert hit test point to the target view
let convertedPoint = convert(point, to: targetView)
// Verify that the target view can receive the touch
if let hitTargetView = targetView.hitTest(convertedPoint, with: event) {
// Forward the touch to the target view
return hitTargetView
}
}
}
// Return the original hit test view - this is the super value - our implmentation didn't affect the hit test pass
return hitTestView
}
}
© 2022 - 2024 — McMap. All rights reserved.