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:
- The
UIToolbar
can be made to slide on and off the screen - 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:
Call the
UIView (subclass)
insteadViewerOverlayView
The
UIWebView
is calledViewerWebView
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:
Instantiate the
ViewerOverlayView
and add it as a subview of the view controller'sview
propertyInstantiate the view controller's
ViewerWebView
instance and insert it at the bottom of the subviews arraySet 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.
Okay, what about this:
-In your custom
UIView
subclass, add a pointer to yourUIWebView
-Add an ivar to the
UIView
subclass,BOOL hasMoved;
-In the
UIView
subclass, override the touch methods like this: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 thetouchesBegan:
method to as delay to handle double tap situations, but I think the general idea is sound...perhaps?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:
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:
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!
Take 2: same as above, but override the methods as follows:
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!
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 theUIViewController.view
. TheUIWebView
remains a direct subview also.The
UIViewController.view
should be a subclass ofUIView
, and it needs to overrideThe 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: