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.
[event allTouches]
array is empty. Because of the empty array, I have no way of issuing a notification to cancel the timer, for example. – Reflection