[iPhone] detecting a hit in a transparent area

Problem : let’s say you want to have a zone tht’s partially transparent and you want to know if a hit is on the non-transparent zone or not.

Under Mac OS X, you can use several methods to do so, but on the iPhone, you’re on your own.

Believe it or not, but the solution came from the past : the QuickDraw migration guide to Carbon actually contained a way to detect transparent pixels in a bitmap image. After some tweaking, the code works.

Here is the setup :
– A view containing a score of NZTouchableImageView subviews (each being able to detect if you are in a transparent zone or not)
– on top of it all, not necessary for every purpose, but needed in my case, a transparent NZSensitiveView that intercepts hits and finds out which subview of the “floorView” (the view with all the partially transparent subviews) was hit
– a delegate conforming to the NZSensitiveDelegate protocol, which reacts to hits and swipes.

The code follows. If you have any use for it, feel free to do so. The only thing I ask in return is a thanks, and if you find any bugs or any way to improve on it, to forward it my way.

Merry Christmas!

[UPDATE] It took me some time to figure out what was wrong and even more to decide to update this post, but thanks to Peng’s questions, I modified the code to work in a more modern way, even with the Gesture Recognizer and the scaling active. Enjoy again!

[UPDATE] Last trouble was linked to the contentsGravity of the images: when scaled to fit/fill, the transformation matrix is not updated, and there’s no real way to guess what it might be. Changing approach, you can trust the CALayer’s inner workings. Enjoy again again!

NZSensitiveDelegate:

@protocol NZSensitiveDelegate
 
- (void) userSlidedLeft:(CGFloat) s;
- (void) userSlidedRight:(CGFloat) s;
- (void) userSlidedTop:(CGFloat) s;
- (void) userSlidedBottom:(CGFloat) s;
 
- (void) userTappedView:(UIView*) v;
 
@end

NZSensitiveView:

@interface NZSensitiveView : UIView {
  id _sdelegate;
  UIView *_floorView;
}
 
@property(retain,nonatomic) IBOutlet id  _sdelegate;
@property(retain,nonatomic) UIView *_floorView;
 
@end
#define kSwipeMinimum 12
#define kSwipeMaximum 4
 
static UIView *currentlyTouchedView;
static CGPoint lastPosition;
static BOOL moving;
 
@implementation NZSensitiveView
@synthesize _sdelegate;
@synthesize _floorView;
 
- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
  // Initialization code
  }
  return self;
}
 
- (void)drawRect:(CGRect)rect {
  // Drawing code
}
 
- (void)dealloc {
  [super dealloc];
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *cTouch = [touches anyObject];
  CGPoint position = [cTouch locationInView:self];
  UIView *roomView = [self._floorView hitTest:position
    withEvent:nil];
 
  if([roomView isKindOfClass:[NZTouchableImageView class]]) {
    currentlyTouchedView = roomView;
  }
 
  moving = YES;
  lastPosition = position;
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *cTouch = [touches anyObject];
  CGPoint position = [cTouch locationInView:self];
 
  if(moving) { // as should be
    if( (position.x - lastPosition.x > kSwipeMaximum) && fabs(position.y - lastPosition.y) < kSwipeMinimum ) {
      // swipe towards the left (moving right)
      [self._sdelegate userSlidedLeft:position.x - lastPosition.x];
      [self touchesEnded:touches withEvent:event];
    } else if( (lastPosition.x - position.x > kSwipeMaximum) && fabs(position.y - lastPosition.y) < kSwipeMinimum ) {
      // swipe towards the right
      [self._sdelegate userSlidedRight:lastPosition.x - position.x];
      [self touchesEnded:touches withEvent:event];
    } else if( (position.y - lastPosition.y > kSwipeMaximum) && fabs(position.x - lastPosition.x) < kSwipeMinimum ) {
      // swipe towards the top
      [self._sdelegate userSlidedTop:position.y - lastPosition.y];
      [self touchesEnded:touches withEvent:event];
    } else if( (lastPosition.y - position.y > kSwipeMaximum) && fabs(position.x - lastPosition.x) < kSwipeMinimum ) {
      // swipe towards the bottom
      [self._sdelegate userSlidedBottom:lastPosition.y - position.y];
      [self touchesEnded:touches withEvent:event];
    }
  }
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *cTouch = [touches anyObject];
  CGPoint position = [cTouch locationInView:self];
  UIView *roomView = [self._floorView        hitTest:position
    withEvent:nil];
  if(roomView == currentlyTouchedView) {
    [self._sdelegate userTappedView:currentlyTouchedView];
  }
 
  currentlyTouchedView = nil;
  moving = NO;
}
 
@end

NZTouchableImageView:

@interface NZTouchableImageView : UIImageView {
}
@end
@implementation NZTouchableImageView
 
- (BOOL) doHitTestForPoint:(CGPoint)point {
    CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo info = kCGImageAlphaPremultipliedLast;
 
    UInt32 bitmapData[1];
    bitmapData[0] = 0;
 
    CGContextRef context =
    CGBitmapContextCreate(bitmapData,
                          1,
                          1,
                          8,
                          4,
                          colorspace,
                          info);
 
    // draw the image into our modified context
    // CGRect rect = CGRectMake(-point.x, 
        //                             point.y - CGImageGetHeight(self.image.CGImage),
        //                             CGImageGetWidth(self.image.CGImage),
        //                             CGImageGetHeight(self.image.CGImage));
        // CGContextDrawImage(context, rect, self.image.CGImage);
    CGContextTranslateCTM(context, -point.x, -point.y);
    [self.layer renderInContext:context];
 
    CGContextFlush(context);
 
    BOOL res = (bitmapData[0] != 0);
 
    CGContextRelease(context);
    CGColorSpaceRelease(colorspace);
 
    return res;
}
 
#pragma mark -
 
- (BOOL) isUserInteractionEnabled {
  return YES;
}
 
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return [self doHitTestForPoint:point];
}
 
@end
  

Of NSURL

While making RemoteTickets, I had to depend heavily on NSURL. For some reason, it didn’t work with Dam’s RT system.

After a couple hours of debugging, here’s the thing: NSURL doesn’t work as expected.

URLWithString:relativeToURL:
Creates and returns an NSURL object initialized with a base URL and a relative string.

+ (id)URLWithString:(NSString *)URLString relativeToURL:(NSURL *)baseURL

Parameters

* URLString
The string with which to initialize the NSURL object. May not be nil. Must conform to RFC 2396. URLString is interpreted relative to baseURL.

* baseURL
The base URL for the NSURL object.

Return Value
An NSURL object initialized with URLString and baseURL. If URLString was malformed, returns nil.

Discussion
This method expects URLString to contain any necessary percent escape codes.

Availability
Available in iOS 2.0 and later.

Seems to me it means I’m building an URL based on concatenation. Well, that’s not the case:

NSURL *baseURL = [NSURL URLWithString:@"http://www.apple.com/macosx"];
NSURL *compositeURL = [NSURL URLWithString:@"/lion" relativeToURL:baseURL];

should give http://www.apple.com/macosx/lion, right? It gives http://www.apple.com/lion instead. The baseURL parameter is actually taken as “the base URL to take the base from”.

The following code gives the result posted afterwards. Use with caution.

    NSURL *baseURL = [NSURL URLWithString:@"http://www.apple.com/macosx/"];
 
    NSURL *relativeURL = [NSURL URLWithString:@"/lion" relativeToURL:baseURL];
    NSURL *relativeURL2 = [NSURL URLWithString:[NSString stringWithFormat:@"%@/lion", [baseURL absoluteString]]];
 
    NSURL *relativeURLLvl211 = [NSURL URLWithString:@"/new" relativeToURL:relativeURL];
    NSURL *relativeURLLvl221 = [NSURL URLWithString:@"/new" relativeToURL:relativeURL2];
 
    NSURL *relativeURLLvl212 = [NSURL URLWithString:[NSString stringWithFormat:@"%@/new", [relativeURL absoluteString]]];
    NSURL *relativeURLLvl222 = [NSURL URLWithString:[NSString stringWithFormat:@"%@/new", [relativeURL2 absoluteString]]];
 
        NSLog(@"%@", [NSString stringWithFormat:@"baseURL:\n%@ (%@)\n\nLevel 1:\nRelative:\n%@ (%@)\nAbsolute:\n%@ (%@)\n\nLevel2:\nRelative/Relative:\n%@ (%@)\nRelative/Absolute:\n%@ (%@)\nAbsolute/Relative(a):\n%@ (%@)\nAbsolute/Absolute:\n%@ (%@)\n",
                     baseURL, [baseURL absoluteString],
                     relativeURL, [relativeURL absoluteString],
                     relativeURL2, [relativeURL2 absoluteString],
                     relativeURLLvl211, [relativeURLLvl211 absoluteString],
                     relativeURLLvl221, [relativeURLLvl221 absoluteString],
                     relativeURLLvl212, [relativeURLLvl212 absoluteString],
                     relativeURLLvl222, [relativeURLLvl222 absoluteString]]);


baseURL:
http://www.apple.com/macosx/ (http://www.apple.com/macosx/)

Level 1:
Relative:
/lion -- http://www.apple.com/macosx/ (http://www.apple.com/lion)
Absolute:
http://www.apple.com/macosx//lion (http://www.apple.com/macosx//lion)

Level2:
Relative/Relative:
/new -- http://www.apple.com/lion (http://www.apple.com/new)
Relative/Absolute:
/new -- http://www.apple.com/macosx//lion (http://www.apple.com/new)
Absolute/Relative(a):
http://www.apple.com/lion/new (http://www.apple.com/lion/new)
Absolute/Absolute:
http://www.apple.com/macosx//lion/new (http://www.apple.com/macosx//lion/new)

  

RemoteTickets

This project is not secret anymore…

For years now I have been using RT (from bestpractical) to track bugs/issues/ideas with my various projects. I like the simplicity of it, and I like the fact that it’s email- and web- editable. With the huge number of things I had to track, and their somewhat urgent nature, I used the mail to keep track of it, through folders and suchlike. But the sheer number of systems and issues made that quite difficult to follow.

My two current project both involve heavy web interaction, so I figured an iPhone front end for RT would not be that difficult to make. It was and it wasn’t, but I’m starting to get results.

Right now, the application is in its last stages of development, early stage of usable beta.

Features:

  • multiple instances tracking (servers or vhosts)
  • full support in reading the tickets (with attachments, links, etc) insofar as the iPhone can handle the file types
  • searching, sorting, categories and queues are easily accessible
  • partial edition support (adding a comment, changing status, queue, due date etc…)
  • new ticket creation
  • calendar view for due dates

Known bugs:
A few crashes due to CoreData not being thread safe
A few unknowable states in the sync engine

The application will be on the AppStore next month, hopefully.

Feel free to comment or ask questions here or via email!

  

WIT

What Is That is out!

For the past few months, in between projects and during unsocial hours, my project partner and I have been toying with the concept, fine tuning the tech and testing testing testing.

Two points in particular took us a long time and make me really proud of my work:

  • You can send an email containing all the information about that particular picture (name, date, gps location, etc), both for people who also have the app and people who don’t. Yep, that’s right, if you have the app, you can import it right back in WIT directly from the email.
  • Both that email feature and the map locator are retro-compatible 2.2.1 and 3.0. I know most people upgraded anyway, but I started developing that thing under 2.2.1 and I wanted to make sure 99% of the population (is there anyone under 1.3?) could use it.

Check it out, buzz it in, and don’t hesitate to comment, here or on the AppStore!

WhatIsThat?
WhatIsThat?
  

Regarding the AppStore for iPhone/iPod Touch

I was reading John Gruber’s piece on Opera Mini, and although, as usual, John is pretty thorough with his analysis, the last sentence made me jump.

Again, though, just because an app doesn’t violate the rules doesn’t mean Apple will accept it.

It kind of invalidates everything everybody says or writes about the AppStore. And the sad thing is that it is true. I wrote an app for Rebel Software (Spin the Bottle, in case anybody’s wondering). We were pretty happy with it, it respected what we thought were the guidelines, was pretty good looking, if not “useful” (it’s just a game, people). It was also the first app doing that on the Store.

It took 6 weeks to validate the app. And a couple more after I fixed a bug they had uncovered. So on the one hand, the validation process is actually pretty thorough, or so it seems. On the other hand, while the app was being reviewed, 4 other spins were released. They are simpler, true. But come on!

The whole mechanism looks good on paper. Apple “filters” bad applications, or buggy ones. The software developer doesn’t have to worry about the installation/update system on the device. And the user has access to everything pretty easily.

But the whole process is very opaque. How comes it took so long? No one knows. How comes some apps are rejected although they follow the guidelines? No one knows.

I’m not saying that every app should be accepted immediately, or that Apple shouldn’t reserve the right to forbid an app to be run on their device. It’s their business model, their choice. I may not agree for philosophical reasons, but from a different point of view, I guess it makes some sort of sense.

What I’m saying is that making the validation process opaque is a mistake. With a clear set of rules, some apps would not even be submitted. Less work for Apple, less work for the developers who won’t even try. When my app was stuck, a contact of mine told me I should have said something, and that he would’ve accelerated the process a bit. Why should I need that? And what happens to a developer who knows nobody within the Forbidden City?

The whole thing looks fishy. People with ideas don’t spin them up because it might get rejected (and working on something for no result costs money). People with bad ideas but having some pull might get some apps accepted although they probably shouldn’t be if you have Standards. The whole thing takes too much time and brain power from everybody.

I talked with a friend about the possibility to submit a prototype to Apple before going any further to see if the app had any chance of being rejected. He replied he wouldn’t do that because he’s afraid that someone else might take idea and implement it before he has time to do so. And while some applications get rejected because they do something too close to the built-in apps, you still can find several applications that do the exact same thing, competing against each other. Why doesn’t he trust Apple to respect the secrecy on that?

So the fog, instead of creating a healthy competition environment, looks like it’s promoting arbitrary, or network-b(i)ased decisions. Transparency is not optional here. Some rejection rules might be unfair, true. But why be ashamed? If the rule is written down in a way that non-lawyers can understand, they will be obeyed. Everybody wins.

OK, I’m naive. OK, I’m optimistic. But either you trust the users not to buy the apps that suck (don’t laugh), or you find a straightfoward way to define what an acceptable app is. Spread the responsibility around a bit. We all want the platform to the be the best there is. Why not work together?