Found my TTPhotoView bug

TTPhotoView bug ExampleThis is a tip for anyone hacking TTPhotoViewController, and specifically TTPhotoView. If you’re finding swiping only rotating between three photos, and if there’s a mix of landscape and portrait then it gets a redraw bug that looks like this:

then you’ve probably disabled updateLayer (perhaps because it wasn’t working for photos with orientations other than Up) but didn’t replace it with [self setNeedsDisplay]. You’re welcome.
Also, for anyone else trying to untie TTPhotoView from TTURLCache and instead work with images you can’t load asynchronously: don’t. Instead make it work with fake URLs, force-feeding the cache as you go. Hey ALAssetLibrary, thanks for being async in all the wrong places.


A multitouch UIPanGestureRecognizer trick

Even though I wanted our app to still support iPhone OS 3.1, I realized that since our extra gesture-based features are iOS4 only, I could use UIGestureRecognizer for those. I already had a UIView subclass I was using as a transparent overlay where I implemented my custom tap detection and drag tracking. What I did was bite the bullet and replaced it completely with a plain UIView with gesture recognizers attached. In a few minutes I was able to duplicate all the behaviour that took lots of trial and error to get right. Kudos to everyone at Apple who worked on UIGestureRecognizer.

One part of the new behaviour I wanted to try out was a drag that started with two fingers. It looked like UIPanGestureRecognizer was working but it was ending the gesture when one finger was lifted. I wanted to support starting with a two-finger tap but then still track if the user lifts one finger.

I thought I’d have to implement a custom recognizer subclass when I came upon a little hack that seems to work: In my action method, I change the minimumNumberOfTouches property from 2 to 1 on the fly when called in the Began state, then set it back to 2 when called with Ended or Cancelled.

- (void)drag2Fingers:(UIPanGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan) {
        sender.minimumNumberOfTouches = 1;
        ...
    } else if (sender.state == UIGestureRecognizerStateEnded ||
               sender.state == UIGestureRecognizerStateCancelled) {
        sender.minimumNumberOfTouches = 2;
        ...
    }
}

This seems to work but maybe it’s presumptuous to think it’s always going to. I think I’ll be doing that custom recognizer subclass in a future update.


Intercepting UINavigationController back button

I was looking for a simple way to add a UINavigationBar to my a modal view controller that presents a normal (pointy) Back button as if there was a root view controller to go back to. It would be as if my outer view was the root view controller.

Now I know one legitimate way of doing this would be to wrap a UINavigationController around my existing outer view an hiding the navigation bar, but that would be an intrusive change to my view class that I didn’t want to change. Another way would be to create UINavigationBar and have my view controller manage it, but this seemed like extra work I was hoping to avoid. I was looking for a self-contained, sneaky way of using a UINavigationController within my modal view controller class’ code.

The two issues were: giving the navigation bar a pointy Back button, and getting called when the user presses the button.

Since it seemed impossible to create a left-pointing button on-demand, I figured the first issue could most easily be solved by making a fake root view controller then pushing my modal view controller onto the UINavigationController’s stack. To keep this detail private to my modal view controller, I made a public method containerViewController that assembles this UINavigationController & fake root view. My caller uses it like:


modalViewController = [[MyModalTableViewController alloc] init];
modalViewController.delegate = whatever;
// and other setup, notification observers added, and such
[myViewController presentModalViewController:
    [modalViewController containerViewController] animated:YES];
// rather than presenting modalViewController itself

To intercept the back button, I searched the net to find a solution. One of the top hits suggests subclassing UINavigationController and overriding - [UIViewController popViewControllerAnimated:]. This worked but I was saw that the UINavigationController’s pop animation back to the fake root controller was still happening, which was interfering with my delegate’s for dismissing the modal view. I needed to find another override that could prevent the pop animation.

I noticed that UINavigationController is documented to be the delegate of UINavigationBar, even though it isn’t declared to be in the header file. I figured this meant UINavigationController implemented the UINavigationBarDelegate methods, one of which is - [UINavigationBarDelegate navigationBar:shouldPopItem:] and I found that indeed my sneaky UINavigationController subclass did indeed get a call to this method.

Here the code I made to do what I wanted:


@interface UINavigationController (UndocumentedUINavigationBarDelegateMethod)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item;
@end
@interface SneakyNavigationController : UINavigationController
@end

@implementation MyModalTableViewController
. . .
- (UIViewController *)containerViewController {
  UIViewController *fakeRootView = [[[UIViewController alloc] init] autorelease];
  fakeRootView.navigationItem.title = NSLocalizedString(@"Back", @"Modal TableView Back button");
  MyModalTableViewController *navController = [[[MyModalTableViewController alloc] initWithRootViewController:fakeRootView] autorelease];
  navController.navigationBar.barStyle = UIBarStyleBlack;
  [navController pushViewController:self animated:NO];
  return navController;
}
@end

@implementation SneakyNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
  UIViewController *viewController = nil;
  for (viewController in self.viewControllers)
    if (viewController.navigationItem == item)
      break;
  if ([viewController isKindOfClass:[MyModalTableViewController class]]) {
    [(ManageSocialAccountsViewController *)viewController close];
    return NO;
  }
  return [super navigationBar:navigationBar shouldPopItem:item];
}
@end


Mysterious twitpic authentication problem fixed

Error 401 from twitpic “Could not authenticate you (header rejected by twitter).”

Thanks to http://shkspr.mobi/blog/index.php/2010/05/howto-twitpic-and-oauth/ and i found that the OAuth header must be created with “GET” even though the request to twitpic is sent as a “POST”. That was unexpected and it probably would have taken me quite a while to find this just going by the documentation.


Improvement to Trevor Harmon’s UIImage+Resize.m

from the “Resize a UIImage the right way” post on Trevor’s Bike Shed
Trevor says:
December 24, 2009 at 9:18 pm
True; I didn’t bother with handling the imageRotation setting in croppedImage. I changed the method to include a source code comment explaining this.
If anyone would like to contribute an improved croppedImage function that fixes this oversight, I’d be happy to include it in the distribution.
I found a good post on Robert Clark’s Niftybean blog, “Selecting regions from rotated EXIF images on iPhone” with code for adapting the a crop rect based on a UIImage’s imageRotation. Adding this to croppedImage I then only needed to finally rotate the resulting CGImageRef, for I only needed to slightly generalize the private resizeImage:transform:… helper method and then use it to rotate the resulting cropped image to match imageRotation. It would be much better if the original UIImage’s metadata, imageRotation included, could be retained so the final rotation step could be avoided.
// Returns a copy of this image that is cropped to the given bounds.
// The bounds will be adjusted using CGRectIntegral.
// JPMH-This method no long ignores the image's imageOrientation setting.
- (UIImage *)croppedImage:(CGRect)bounds {
    CGAffineTransform txTranslate;
    CGAffineTransform txCompound;
    CGRect adjustedBounds;
    BOOL drawTransposed;
    
    switch (self.imageOrientation) {
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
            txTranslate = CGAffineTransformMakeTranslation(self.size.width, self.size.height);
            txCompound = CGAffineTransformRotate(txTranslate, M_PI);
            adjustedBounds = CGRectApplyAffineTransform(bounds, txCompound);
            drawTransposed = NO;
            break;
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            txTranslate = CGAffineTransformMakeTranslation(self.size.height, 0.0);
            txCompound = CGAffineTransformRotate(txTranslate, M_PI_2);
            adjustedBounds = CGRectApplyAffineTransform(bounds, txCompound);
            drawTransposed = YES;
            break;
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
            txTranslate = CGAffineTransformMakeTranslation(0.0, self.size.width);
            txCompound = CGAffineTransformRotate(txTranslate, M_PI + M_PI_2);
            adjustedBounds = CGRectApplyAffineTransform(bounds, txCompound);
            drawTransposed = YES;
            break;
        default:
            adjustedBounds = bounds;
            drawTransposed = NO;
    }
    
    CGImageRef imageRef = CGImageCreateWithImageInRect([self CGImage], adjustedBounds);
    UIImage *croppedImage;
    if (CGRectEqualToRect(adjustedBounds, bounds))
        croppedImage = [UIImage imageWithCGImage:imageRef];
    else
        croppedImage = [self resizedImage:imageRef
                                     size:bounds.size
                                transform:[self transformForOrientation:bounds.size]
                           drawTransposed:drawTransposed
                     interpolationQuality:kCGInterpolationHigh];
    CGImageRelease(imageRef);
    return croppedImage;
}
Where the resizeImage: utility method was change to take a CGImageRef as a parameter instead of always using self.CGImage:
- (UIImage *)resizedImage:(CGImageRef)imageRef
                     size:(CGSize)newSize
                transform:(CGAffineTransform)transform
           drawTransposed:(BOOL)transpose
     interpolationQuality:(CGInterpolationQuality)quality;
- (CGAffineTransform)transformForOrientation:(CGSize)newSize;