What works to pass touches through to (and display

2019-03-16 04:56发布

问题:

I have the following subview chain:

UIViewController.view -+
                       |-> UIView (subclass) -+
                       |                      +-> UIToolbar 
                       |
                       +------------------------> UIWebView

In the subclass, I override its -touchesEnded:forEvent: method in order to hide and show the UIToolbar on a single tap touch, through a CAAnimation, as well as issue an NSNotification that causes the view controller to hide its navigation bar.

If I do not add the UIWebView as a subview to the view controller's view, then this works properly.

If I then add the UIWebView as a subview of the view controller's view, then the UIToolbar does not appear, and I do not get the animation effect. The UIWebView responds to touches but the subclassed UIView does not. The notification does get fired, though, and the navigation bar does get hidden.

What is the best way to arrange these subviews so that:

  1. The UIToolbar can be made to slide on and off the screen
  2. The UIWebView is visible can still receive its typical touch-events: zoom in/out and double-tap to reset zoom level

A correct answer will meet both criteria.


EDIT

I made some progress with this, but I cannot distinguish touch events and thus I fire a toolbar hide/show method when it should not be fired.

I set the view controller's view property to the UIView subclass, and I inserted the web view at the top of the -subviews array via [self.view insertSubview:self.webView atIndex:0].

I added the following method to the UIView subclass:

- (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
    UIView *subview = [super hitTest:point withEvent:event];

    if (event.type == UIEventTypeTouches) {
        [self toggleToolbarView:self];

    // get touches
    NSSet *touches = [event allTouches];

    NSLog(@"subview: %@", subview);
    NSLog(@"touches: %@", touches);

    // individual touches
    for (UITouch *touch in touches) {
        switch (touch.phase) {
            case UITouchPhaseBegan:
                NSLog(@"UITouchPhaseBegan");
                break;
            case UITouchPhaseMoved:
                NSLog(@"UITouchPhaseMoved");
                break;
            case UITouchPhaseCancelled:
                NSLog(@"UITouchPhaseCancelled");
                break;
            case UITouchPhaseStationary:
                NSLog(@"UITouchPhaseStationary");
                break;      
            default:
                NSLog(@"other phase...");
                break;
            }
        }
    }

    return subview;
}

The method -toggleToolbarView: triggers an NSTimer-timed CAAnimation that hides/shows a UIToolbar*.

The problem is that a touch-drag combination causes -toggleToolbarView: to be fired (as well as zooming in or out of the web view). What I would like to do is cause -toggleToolbarView: to be fired only when there is a touch-only event.

When I call the above method -hitTest:withEvent:, it looks like the UIWebView sucks up the touches. The two NSLog statements show that the UIView* that is returned from the parent UIView*'s -hitTest:withEvent: is either the UIWebView or the UIToolbar (when it is on-screen and touched). The touches array is empty and therefore the touch event types cannot be distinguished.

There's one more thing I will try, but I wanted to record this attempt here in case others have quick suggestions. Thanks for your continued help.


EDIT II

I made some progress with getting the UIWebView to pinch/zoom and respond to double-taps. I can also hide/show the toolbar with single-taps.

Instead of messing with the UIWebView instance's private UIScroller, I access its inner, private UIWebDocumentView. But the web view as a whole does not redraw properly after zooming in.

What this means: If my document contains a PDF or an SVG vector illustration, for example, zooming in causes the contents to be blurry, as if the image tiles that make up the rendered PDF or vector illustration contents are not being re-rendered at the new zoom factor.

For the purposes of documenting this, I will:

  1. Call the UIView (subclass) instead ViewerOverlayView

  2. The UIWebView is called ViewerWebView

Both are subclasses of UIView and UIWebView, respectively.

My ViewerWebView contains a new ivar (with @property + @synthesized):

UIView *privateWebDocumentView;

(This UIView is actually a pointer to a UIWebDocumentView instance, but to avoid Apple flagging the app for refusal I cast this to a UIView for the simple purpose of passing it touch events.)

The implementation of ViewerWebView overrides -hitTest:withEvent:

- (UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *_subview = [super hitTest:point withEvent:event];
    self.privateWebDocumentView = _subview;
    return _subview;
}

My ViewerOverlayView contains new ivars (with @property + @synthesized):

ViewerWebView *webView;
UIView *webDocumentView;

In the implementation of the overlay view, I override -touchesBegan:withEvent:, -touchesMoved:withEvent: and -touchesEnded:withEvent:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.touchHasMoved = NO;
    if (self.webView) {
        UITouch *touch = [touches anyObject];
        CGPoint touchPoint = [touch locationInView:self];
        self.webDocumentView = [self.webView hitTest:touchPoint withEvent:event];
    }
    [super touchesBegan:touches withEvent:event];
    [self.webDocumentView touchesBegan:touches withEvent:event];
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    self.touchHasMoved = YES;
    if (self.webView) {
        UITouch *touch = [touches anyObject];
        CGPoint touchPoint = [touch locationInView:self];
        self.webDocumentView = [self.webView hitTest:touchPoint withEvent:event];
    }
    [super touchesMoved:touches withEvent:event];
    [self.webDocumentView touchesMoved:touches withEvent:event];
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if(!self.touchHasMoved) 
        [self toggleToolbarView:self];
    if (self.webView) {
        UITouch *touch = [touches anyObject];
        CGPoint touchPoint = [touch locationInView:self];
        self.webDocumentView = [self.webView hitTest:touchPoint withEvent:event];
    }
    [super touchesEnded:touches withEvent:event];
    [self.webDocumentView touchesEnded:touches withEvent:event];
}

In the view controller, I do the following:

  1. Instantiate the ViewerOverlayView and add it as a subview of the view controller's view property

  2. Instantiate the view controller's ViewerWebView instance and insert it at the bottom of the subviews array

  3. Set the ViewerOverlayView web view property to the view controller's web view

So now my transparent ViewerOverlayView correctly passes zoom/stretch/double-tap touch events to the ViewerWebView instance's private UIWebDocumentView instance. The UIWebDocumentView then resizes.

However, the content does not redraw itself with the new scale. So vector-based content (e.g. PDF, SVG) looks blurry when zooming in.

Here's what the view looks like without scaling, nice and sharp:

Here's what the view looks like when zoomed in, which is not as sharp as it would otherwise be without all this custom event munging:

Interestingly, I can only rescale between two zoom levels. Once I stop double-tap-stretching to zoom in at any level, it then "snaps back" to the frame you see in the image above.

I tried -setNeedsDisplay on the ViewerWebView and its inner UIWebDocumentView instance. Neither method call worked.

Despite wanting to avoid private methods, because I want to eventually get this app approved, I also tried this suggestion of accessing the private UIWebDocumentView method -_webCoreNeedsDisplay:

[(UIWebDocumentView *)self.webDocumentView _webCoreNeedsDisplay];

This method caused the app to crash from an unrecognized selector exception. Perhaps Apple has changed the name of this method from SDK 3.0 to 3.1.2.

Unless there is a way to cause the web view to redraw its contents, I guess I'm going to have to scrap the transparent overlay view and just draw a web view, in order to get the web view to work properly. That sucks!

If anyone has thoughts about redrawing-after-scaling, please feel free to add an answer.

Thanks again to all of you for your input. As an aside, I did not cancel the bounty on this question -- I do not know why it was not automatically awarded to the first answer to this thread, as I was otherwise expecting to happen from previous experience.


EDIT III

I found this web page that shows how to subclass the application window so as to observe taps and forward those taps to the view controller.

It's not necessary to implement its delegate everywhere throughout the application, only where I want to observe taps on UIWebView, so this may work really well for a document viewer.

So far, I can observe taps, hide and show the toolbar, and the UIWebView behaves like a web view. I can zoom in and out of the web view and the document is re-rendered properly.

While it would be nice to learn how to manage taps in a more generic fashion, this looks like a pretty great solution.

回答1:

The way I'm reading your question, you want to intercept the events to trigger some action (animate toolbar, post notification), while allowing the event to also reach its natural destination.

If I were trying to do this, I would put the UIToolbar directly as a subview of the UIViewController.view. The UIWebView remains a direct subview also.

The UIViewController.view should be a subclass of UIView, and it needs to override

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

The view can side-step being part of the event processing by sending back the view that you want to receive the event (UIToolbar or UIWebView), while you still get a chance to trigger the actions you want.

An example might be:

- (UIView *)hitTest:(CGPoint) point withEvent:(UIEvent *)event {
    UIView* subview = [super hitTest:point withEvent:event];
    /* Use the event.type, subview to decide what actions to perform */
    // Optionally nominate a default event recipient if your view is not completely covered by subviews
    if (subview == self) return self.webViewOutlet;
    return subview;
}


回答2:

If you start by adding the UIView subclass to the view controller's view, then add the UIWebView as you describe, the UIWebView is going to completely cover the UIView subclass (assuming both views are completely overlapping as you mention in the question comments).

It sounds like you don't want that - you want the subclass above the web view. You need to either add them to the view controller's view in the opposite order (add web view, then add custom UIView) or call:

[viewContoller.view insertSubview:webView belowSubview:subclass];

With that out of the way, let's talk about how to intercept the touch events. I'm going to suggest a different approach that allowed me to do something kind of similar. Here's the question and answer I created for it - feel free to look there for more details on this technique.

The idea is that you subclass UIApplication and override the -sendEvent: method. Yours will look something like this, I suppose:

- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];

    NSSet *allTouches = [event allTouches];
    if ([allTouches count] > 0) {
        UITouch *touch = [allTouches anyObject];
        if ([allTouches count] == 1 && touch.phase == UITouchPhaseEnded && touch.tapCount == 1) {
            // Do something here
        }
    }
}

I don't expect this will get you 100% there, but hopefully it's a start. In particular, you may want to play around with the if condition that checks the properties of the UITouches you're dealing with. I'm not sure if I'm including the right checks or if they'll catch the exact condition you're looking for.

Good luck!



回答3:

Okay, what about this:

-In your custom UIView subclass, add a pointer to your UIWebView

-Add an ivar to the UIView subclass, BOOL hasMoved;

-In the UIView subclass, override the touch methods like this:

-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    hasMoved = NO;
    [webView touchesBegan:touches withEvent:event];
}

-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    hasMoved = YES;
    [webView touchesMoved:touches withEvent:event];
}

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if(!hasMoved) [self hideToolbar];
    [webView touchesEnded:touches withEvent:event];
}

And then put the custom view on top of the web view. Seems too simple, and it probably is -- some magic may be required to handle multiple touches correctly, and you may need to use a NSTimer in the touchesBegan: method to as delay to handle double tap situations, but I think the general idea is sound...perhaps?



回答4:

Take 2: same as above, but override the methods as follows:

-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    hasMoved = NO;
    [[[webView subviews] objectAtIndex:0] touchesBegan:touches withEvent:event];
}

-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    hasMoved = YES;
    [[[webView subviews] objectAtIndex:0] touchesMoved:touches withEvent:event];
}

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if(!hasMoved) [self toggleToolbar];
    [[[webView subviews] objectAtIndex:0] touchesEnded:touches withEvent:event];
}

-(void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [[[webView subviews] objectAtIndex:0] touchesCancelled:touches withEvent:event];
}

This works...sort of. This allows you to drag the web view around. But it isn't handling other touch events properly (following links, pinching, anything). So. Probably not very useful, but it was progress to me!