365Cocoa

A snippet of Objective-C code per day, 365 days long.
Written by Pieter Omvlee, developer and owner of Bohemian Coding.

May 20

Day 54: Tedious Rounded Views

Last post I wrote about transparent windows. I concluded that you only needed to write your own content view to make the window in any shape you like. Let’s suppose we want to have a rounded view with a pointing arrow on one of the four sides. The code is a bit tedious but might be a nice example of doing graphics in code wherever possible:

First we want to know the area in the view that will be the rounded rectangle, so we subtract on the specific side the width/height of the arrow. The contstants used should be self-explanatory.

- (NSRect)availableContentRect
{
  NSRect contentRect = [self bounds];
  
  if (arrowPosition == CHArrowPositionNone)
    return NSZeroRect;
  
  if (arrowPosition == CHArrowPositionLeft) {
    contentRect.origin.x    += kArrowSize;
    contentRect.size.width  -= kArrowSize;
  } else if (arrowPosition == CHArrowPositionTop) {
    contentRect.size.height -= kArrowSize;
  } else if (arrowPosition == CHArrowPositionRight) {
    contentRect.size.width  -= kArrowSize;
  } else if (arrowPosition == CHArrowPositionBottom) {
    contentRect.origin.y    += kArrowSize;
    contentRect.size.height -= kArrowSize;
  }
  
  return contentRect;
}

Second, we construct the path using this rectangle. For those familiar with NSBezierPath it should be easy to understand albeit a bit tedious, as said. Again, the constants used need no explanation.

- (NSBezierPath *)backgroundPath
{
  NSRect rect = [self availableContentRect];
  
  if (NSEqualRects(NSZeroRect, rect))
    return nil;
  
  CGFloat minX = NSMinX(rect);
  CGFloat maxX = NSMaxX(rect);
  CGFloat minY = NSMinY(rect);
  CGFloat maxY = NSMaxY(rect);
    
  NSBezierPath *path = [NSBezierPath bezierPath];
  [path moveToPoint:NSMakePoint(minX, minY+kCornerRadius)];
  
  if ([self hasArrow] && arrowPosition == CHArrowPositionLeft) {
    [path lineToPoint:NSMakePoint(minX, NSMidY(rect)-kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(-kArrowSize, kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(kArrowSize,  kArrowSize)];
  }
  
  [path lineToPoint:NSMakePoint(minX, maxY-kCornerRadius)];
  [path curveToPoint:NSMakePoint(minX+kCornerRadius, maxY)
       controlPoint1:NSMakePoint(minX, maxY-kCornerRadius/2)
       controlPoint2:NSMakePoint(minX+kCornerRadius/2, maxY)];
  
  if ([self hasArrow] && arrowPosition == CHArrowPositionTop) {
    [path lineToPoint:NSMakePoint(NSMidX(rect)-kArrowSize, NSMaxY(rect))];
    [path relativeLineToPoint:NSMakePoint(kArrowSize, kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(kArrowSize, -kArrowSize)];
  }
  
  [path lineToPoint:NSMakePoint(maxX-kCornerRadius, maxY)];
  [path curveToPoint:NSMakePoint(maxX, maxY-kCornerRadius)
       controlPoint1:NSMakePoint(maxX-kCornerRadius/2,maxY)
       controlPoint2:NSMakePoint(maxX, maxY-kCornerRadius/2)];
  
  if ([self hasArrow] && arrowPosition == CHArrowPositionRight) {
    [path lineToPoint:NSMakePoint(maxX, NSMidY(rect)+kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(kArrowSize, -kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(-kArrowSize, -kArrowSize)];
  }
  
  [path lineToPoint:NSMakePoint(maxX, minY+kCornerRadius)];
  [path curveToPoint:NSMakePoint(maxX-kCornerRadius, minY)
       controlPoint1:NSMakePoint(maxX, minY+kCornerRadius/2)
       controlPoint2:NSMakePoint(maxX-kCornerRadius/2, minY)];
  
  if ([self hasArrow] && arrowPosition == CHArrowPositionBottom) {
    [path lineToPoint:NSMakePoint(NSMidX(rect)+kArrowSize, NSMinY(rect))];
    [path relativeLineToPoint:NSMakePoint(-kArrowSize, -kArrowSize)];
    [path relativeLineToPoint:NSMakePoint(-kArrowSize, kArrowSize)];
  }
  
  [path lineToPoint:NSMakePoint(minX+kCornerRadius, minY)];
  [path lineToPoint:NSMakePoint(minX+kCornerRadius, minY)];
  [path curveToPoint:NSMakePoint(minX, minY+kCornerRadius)
       controlPoint1:NSMakePoint(minX+kCornerRadius/2, minY)
       controlPoint2:NSMakePoint(minX,minY+kCornerRadius/2)];
  [path closePath];
  
  return path;
}

The last step of course is to draw the path. This’ll depend on how you’d like your window to look. You can go in all directions; mimic the MAAttachedWindow or the look I choose for in for example SlipCover and Sketch:

Again, a nice example of some custom drawing code using gradients and clipping paths:

- (void)drawRect:(NSRect)rect
{
  [[NSColor clearColor] set];
  NSRectFill([self bounds]);
  
  NSBezierPath *path = [self backgroundPath];
  
  NSColor *startColor  = [NSColor colorWithDeviceWhite:0.871 alpha:0.9];
  NSColor *endColor    = [NSColor colorWithDeviceWhite:1.000 alpha:0.9];
  NSGradient *gradient = [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0, endColor, 1.0, nil];
  [[gradient autorelease] drawInBezierPath:path angle:90];
  
  [NSGraphicsContext saveGraphicsState];
  [[[NSColor whiteColor] colorWithAlphaComponent:0.5] set];
  [path setLineWidth:2.0];
  [path addClip];
  [path stroke];
  [NSGraphicsContext restoreGraphicsState];
}

And with that, I’m taking a few days off…


May 18

Day 53: Transparent windows

A question that seems to come up online from time to time is how to create a window in a special shape; rounded corners or with arrows sticking out on a side much like those popovers on the iPad. I use something similar in Sketch and here’s the basic outline of how to do it:

You start by subclassing NSWindow and overriding the initWith… method. You can also create it in code of course but that means you can’t use IB, up to you. You’ll also see that when we subclass we can do some neat things.

- (id)initWithContentRect:(NSRect)contentRect
                styleMask:(NSUInteger)aStyle
                  backing:(NSBackingStoreType)bufferingType
                    defer:(BOOL)flag
{
  self = [super initWithContentRect:contentRect
                          styleMask:NSBorderlessWindowMask
                            backing:NSBackingStoreBuffered
                              defer:NO];
  if (self) {
    [self setAlphaValue:1.0];
    [self setOpaque:NO];
    [self setHasShadow:YES];
  }
  return self;
}

I’ts important to override the following method to return YES if that’s important to you because by default borderless windows can’t become key:

- (BOOL)canBecomeKeyWindow
{
  return YES;
}

Borderless windows also can’t be moved by the mouse since there’s no titlebar. Do create this behaviour yourself override mouseDown and mouseDragged to do something along the lines of this:

- (void)mouseDown:(NSEvent *)theEvent
{
  initialLocation = [theEvent locationInWindow];
}

- (void)mouseDragged:(NSEvent *)theEvent
{
  NSRect screenFrame = [[NSScreen mainScreen] visibleFrame];
  NSRect windowFrame = [self frame];
  NSPoint newOrigin  = windowFrame.origin;
  
  NSPoint currentLocation = [theEvent locationInWindow];
  newOrigin.x += (currentLocation.x - initialLocation.x);
  newOrigin.y += (currentLocation.y - initialLocation.y);
  
  [self setFrameOrigin:newOrigin];
}

Now you’ll have an empty window. To provide the content for this window we provide our own contentView for the window which will do the actual drawing. Just override -drawRect: and you’re done. One issue you might get is that when you try draw the view in a different way, the window will have cached the shadow of the old view so keep that in mind. I believe resizing the window slightly will do the trick.


May 12

Day 52: changeKeys:inBlock:

If for some reason you’re writing your own getters and setters code, you want to comply to KVO and properly notify any observers of change using willChangeValueForKey: and didChangeValueForKey:. Now this is straightforward enough but starts to get really ugly if in one method you have to notify a change to several keys using first having lets say four willChangeValueForKey: and afterwards again four didChangeValueForKey: is particularly ugly. Especially if you do this in few locations.

This is the soluton I came up with. I also wrote one for just one key but that one is too trivial to mention here separately

- (void)changeKeys:(NSArray *)keys inBlock:(BLOCK)block
{
  for (NSString *key in keys)
    [self willChangeValueForKey:key];
  block();
  for (NSString *key in keys)
    [self didChangeValueForKey:key];
}

May 11

Day 51: KVO+Blocks

Today’s code is not mine, but I do find it incredibly useful. As readers of this blog will know by know, in 10.6 Apple introduced Blocks to Objective-C. I have given plenty examples in the past, but today’s is a favourite of mine.

Andy Matuschak released this code almost a year ago and it’s an incredibly handy way to do Key-Value-Observing (KVO) with Blocks instead of those ugly callbacks. Gone is the need to override obserValueForKeyPath: with a big switch statement to check for the keypaths. This is so much easier:

[self addObserverForKeyPath:@"..." task:^(id obj, NSDictionary *change){
  //...
}];

The source code is hosted on GitHub and you also might want to read Andy’s post for some more info.


May 10

Day 50: Guest post 2, NSResponder goodies

Today’s post was submitted to me some time ago by Vadim Shpakovski from snippetsapp.com. My apologies that it has taken so long to post it, but I am grateful that he wanted to contribute here. So without further ado:

Snow Leopard brought a lot of new APIs that make cool things possible in just a few lines of code. For example, NSResponder class has got a new method named swipeWithEvent:. This method is passed through the first responder chain when you make a 3-fingers swipe gesture on your Multitouch Trackpad or Magic Mouse.

Apple’s Mail.app has one of the most handful implementations for this gesture: 3-fingers swipe allows you to scroll between emails in the main table view. And now you can incorporate this behavior into your Cocoa application really easy. Simply add the following snippet of code to the window controller having a table view:

- (void)handleSwipeBySelectingNextItem:(BOOL)selectNext
{
    // Select next or previous item in the table view
    NSArrayController *listingController = [self dataArrayController];
    if (selectNext)
        [listingController selectNext:self];
    else
        [listingController selectPrevious:self];
    
    // Move selection at the table view and make selected row visible
    NSTableView *listingView = [self dataTableView];
    [self.window makeFirstResponder:listingView];
    [self performSelector:@selector(scrollTableViewToVisibleRow:)
               withObject:listingView afterDelay:0.0f];
}

- (void)scrollTableViewToVisibleRow:(NSTableView *)tableView
{
    [tableView scrollRowToVisible:[tableView selectedRow]];
}

- (void)swipeWithEvent:(NSEvent *)event
{
    if ([event deltaX] < 0 || [event deltaY] < 0)
        [self handleSwipeBySelectingNextItem:YES];
    else if ([event deltaX] > 0 || [event deltaY] > 0)
        [self handleSwipeBySelectingNextItem:NO];
    else
        [super swipeWithEvent:event];
}


May 8

Day 49: Tail follow-up

On Day 47 I posted a snippet with the -tail method and a question was asked in what situation I found this useful. I thought it’d be a nice opportunity to post an example of how I use -tail in Sketch.

This abbreviated snippet of code makes a boolean operation over multiple bezier paths. I already have the code to do an operation on two shapes and using it on more than two shapes is achieved by accumulating the result. Note that the layers variable that is used just holds the layers on which to do a boolean operation.

  NSBezierPath *accumulatedPath = [[layers firstObject] bezierPath];
  for (MSShapeLayer *shape in [layers tail])
    accumulatedPath = [accumulatedPath booleanOp:operation
                                        withPath:[shape bezierPath]];


May 6

Day 48: Guest Post: NSUserDefaults

Today’s post is the first guest post to appear on 365Cocoa. This snippet was suggested by Pierre Bernard from houdah.com, many thanks to him for suggesting it. He has two tricks to share about NSUserDefaults:

  1. Category on NSUserDefaults to send out KVO notifications. Needed on iPhone where there is no NSUserDefaultsController
  2. Use of #define to create setters and getters

@implementation NSUserDefaults (DefaultsManager)
#define DefaultsGetter(defaultType, defaultName) \
  defaultType result = [self objectForKey:defaultName]; \
  return result;

#define DefaultsSetter(defaultType, defaultName, value) \
  [self willChangeValueForKey:defaultName]; \
  if (value != nil) { \
    [self setObject:value forKey:defaultName]; \
  } \
  else { \
    [self removeObjectForKey:defaultName]; \
  } \
  [self didChangeValueForKey:defaultName];

- (NSDictionary*)currentEngine
{
  DefaultsGetter(NSDictionary*, kCurrentEngineKey);
}

- (void)setCurrentEngine:(NSDictionary*)currentEngine
{
  DefaultsSetter(NSDictionary*, kCurrentEngineKey, [[currentEngine copy] autorelease]);
}
@end

Again I want to thank him for suggesting it and if you have something to share yourself, I’ll be happy to post it here in the same way. Another guest post is coming up tomorrow by the way.


May 5

Day 47: Back from break: -tail

I’ve taken a little break for which I apologise; a combination of friends and family visiting and a short holiday. But we’re back.

I often find I need everything except the first item in an array. Instead of starting iterating a for loop at i=1, I wrote this:

- (NSArray *)tail
{
  if ([self count] == 0)
    return [NSArray array];

  return [self objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(1,[self count]-1)]];
}

Apr 28

Day 46: In mourning once again

Apple announced the dates for WWDC2010 today as most of you will know. One of the major disappointments for me is the lack of Apple Design Awards for mac applications.

I wrote about this on my company’s blog and instead of giving a code snippet today, I’m just going to link to this blog post: The center of the app universe


Apr 27

Day 45: trivial nsview programming in fact

I had promised to write about the different ways to do view drawing; one way to get the data to draw a view is to provide standard getters and setters for the object(s) the view should draw. This is however too trivial do write something sensible about I now see.

One tip I can mention is to mark the view dirty whenever you are setting a new object on thr vire because NSView won’t redraw otherwise. I assume you can all write getters and setters or better, synchronised properties, so I won’t spell that out.

The bssic structure of the drawing part also becomes easy once you know what you have to draw. It will be difficult to give an example since it will widely differ for each use.

Using delegates is slightly different; make sure your model is updated and somehow tell the view to reload it’s data similar to how NSTableView works. In fact NSTableViews datasource and delegate methods are a good example of how to write these. Remember to include a reference to the control in the datasource methods in case you later get multiple instances of the same class sharing a single control object.


Page 1 of 6