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:
UIToolbar
can be made to slide on and off the screenUIWebView
is visible can still receive its typical touch-events: zoom in/out and double-tap to reset zoom levelA 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)
instead ViewerOverlayView
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:
Instantiate the ViewerOverlayView
and add it as a subview of the view controller's view
property
Instantiate the view controller's ViewerWebView
instance and insert it at the bottom of the subviews array
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.
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;
}
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!
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With