Draw NSTableView Alternating Rows Like iTunes 11

I am aware that there are other questions on SO about changing alternating row colors. That’s easy and it’s not what I want to do.

I want to draw custom alternating-colored rows in a view-based NSTableView that look like those from iTunes 11 (slight bezel at the top and bottom of the row, as shown in this screenshot):

  • How to make window transparent in osx swift?
  • setNeedsDisplay not working?
  • Can't use reloadData from another class
  • CSV parser with low memory footprint for iPhone
  • Sort NSArray using sortedArrayUsingFunction
  • Integrating gcc 4.8 with Xcode 4.x
  • iTunes 11 screenshot

    NOTE:

    I know I can subclass NSTableRowView and do my custom drawing there. However, this is NOT an acceptable answer because the custom row will only be used for rows that have data in the table. In other words, if the table has only 5 rows, those 5 rows will use my custom NSTableRowView class but the remaining “rows” in the rest of the table (which are empty) will use the standard alternating colors. In that case, the first 5 rows will show the bezel and the remaining ones won’t. Not good.

    So, how can I hack NSTableView to draw these styled alternating rows for both filled and empty rows?

    3 Solutions Collect From Internet About “Draw NSTableView Alternating Rows Like iTunes 11”

    That “slight bezel”, as you put it, can actually be easily done with a little cheating on our part. Because, if you look closely, the top of every cell is a slightly lighter blue color than the dark alternating row, and the bottom of every cell is a dark grayish color, you can subclass NSTableView, then override - (void)drawRow:(NSInteger)row clipRect:(NSRect)clipRect:

    - (void)drawRow:(NSInteger)row clipRect:(NSRect)clipRect
    {
        //Use the drawing code from http://stackoverflow.com/a/5101923/945847, but change the colors to
        //look like iTunes's alternating rows.
        NSRect cellBounds = [self rectOfRow:row];
        NSColor *color = (row % 2) ? [NSColor colorWithCalibratedWhite:0.975 alpha:1.000] : [NSColor colorWithCalibratedRed:0.932 green:0.946 blue:0.960 alpha:1.000];
        [color setFill];
        NSRectFill(cellBounds);
    
        /* Slightly dark gray color */
        [[NSColor colorWithCalibratedWhite:0.912 alpha:1.000] set];
        /* Get the current graphics context */
        CGContextRef currentContext = [[NSGraphicsContext currentContext]graphicsPort];
        /*Draw a one pixel line of the slightly lighter blue color */
        CGContextSetLineWidth(currentContext,1.0f);
        /* Start the line at the top of our cell*/
        CGContextMoveToPoint(currentContext,0.0f, NSMaxY(cellBounds));
        /* End the line at the edge of our tableview, for multi-columns, this will actually be overkill*/
        CGContextAddLineToPoint(currentContext,NSMaxX(cellBounds), NSMaxY(cellBounds));
        /* Use the context's current color to draw the line */
        CGContextStrokePath(currentContext);
    
        /* Slightly lighter blue color */
        [[NSColor colorWithCalibratedRed:0.961 green:0.970 blue:0.985 alpha:1.000] set];
        CGContextSetLineWidth(currentContext,1.0f);
        CGContextMoveToPoint(currentContext,0.0f,1.0f);
        CGContextAddLineToPoint(currentContext,NSMaxX(self.bounds), 1.0f);
        CGContextStrokePath(currentContext);
    
        [super drawRow:row clipRect:clipRect];
    }
    

    Which, when done in a quick little tableview, looks like this:
    Nice Bezeling!

    But what to do about the top and bottom of the tableview? After all, they’ll still be either an ugly white, or the default alternating rows color. Well, as Apple revealed (in a talk titled, interestingly enough View Based NSTableView, Basic To Advanced), you can override -(void)drawBackgroundInClipRect:(NSRect)clipRect and do a little math to draw the background of the tableview like extra rows. A quick implementation looks something like this:

    -(void)drawBackgroundInClipRect:(NSRect)clipRect
    {
        // The super class implementation obviously does something more
        // than just drawing the striped background, because
        // if you leave this out it looks funny
        [super drawBackgroundInClipRect:clipRect];
    
        CGFloat   yStart   = 0;
        NSInteger rowIndex = -1;
    
        if (clipRect.origin.y < 0) {
            while (yStart > NSMinY(clipRect)) {
                CGFloat yRowTop = yStart - self.rowHeight;
    
                NSRect rowFrame = NSMakeRect(0, yRowTop, clipRect.size.width, self.rowHeight);
                NSUInteger colorIndex = rowIndex % self.colors.count;
                NSColor *color = [self.colors objectAtIndex:colorIndex];
                [color set];
                NSRectFill(rowFrame);
    
                /* Slightly dark gray color */
                [[NSColor colorWithCalibratedWhite:0.912 alpha:1.000] set];
                /* Get the current graphics context */
                CGContextRef currentContext = [[NSGraphicsContext currentContext]graphicsPort];
                /*Draw a one pixel line of the slightly lighter blue color */
                CGContextSetLineWidth(currentContext,1.0f);
                /* Start the line at the top of our cell*/
                CGContextMoveToPoint(currentContext,0.0f, yRowTop + self.rowHeight - 1);
                /* End the line at the edge of our tableview, for multi-columns, this will actually be overkill*/
                CGContextAddLineToPoint(currentContext,NSMaxX(clipRect), yRowTop + self.rowHeight - 1);
                /* Use the context's current color to draw the line */
                CGContextStrokePath(currentContext);
    
                /* Slightly lighter blue color */
                [[NSColor colorWithCalibratedRed:0.961 green:0.970 blue:0.985 alpha:1.000] set];
                CGContextSetLineWidth(currentContext,1.0f);
                CGContextMoveToPoint(currentContext,0.0f,yRowTop);
                CGContextAddLineToPoint(currentContext,NSMaxX(clipRect), yRowTop);
                CGContextStrokePath(currentContext);
    
                yStart -= self.rowHeight;
                rowIndex--;
            }
        }
    }
    

    But then, this leaves the bottom of the tableview that same ugly blank white color! So, we have to also override -(void)drawGridInClipRect:(NSRect)clipRect. Yet another quick implementation looks like this:

    -(void)drawGridInClipRect:(NSRect)clipRect {
        [super drawGridInClipRect:clipRect];
    
        NSUInteger numberOfRows = self.numberOfRows;
        CGFloat yStart = 0;
        if (numberOfRows > 0) {
            yStart = NSMaxY([self rectOfRow:numberOfRows - 1]);
        }
        NSInteger rowIndex = numberOfRows + 1;
    
        while (yStart < NSMaxY(clipRect)) {
            CGFloat yRowTop = yStart - self.rowHeight;
    
            NSRect rowFrame = NSMakeRect(0, yRowTop, clipRect.size.width, self.rowHeight);
            NSUInteger colorIndex = rowIndex % self.colors.count;
            NSColor *color = [self.colors objectAtIndex:colorIndex];
            [color set];
            NSRectFill(rowFrame);
    
            /* Slightly dark gray color */
            [[NSColor colorWithCalibratedWhite:0.912 alpha:1.000] set];
            /* Get the current graphics context */
            CGContextRef currentContext = [[NSGraphicsContext currentContext]graphicsPort];
            /*Draw a one pixel line of the slightly lighter blue color */
            CGContextSetLineWidth(currentContext,1.0f);
            /* Start the line at the top of our cell*/
            CGContextMoveToPoint(currentContext,0.0f, yRowTop - self.rowHeight);
            /* End the line at the edge of our tableview, for multi-columns, this will actually be overkill*/
            CGContextAddLineToPoint(currentContext,NSMaxX(clipRect), yRowTop - self.rowHeight);
            /* Use the context's current color to draw the line */
            CGContextStrokePath(currentContext);
    
            /* Slightly lighter blue color */
            [[NSColor colorWithCalibratedRed:0.961 green:0.970 blue:0.985 alpha:1.000] set];
            CGContextSetLineWidth(currentContext,1.0f);
            CGContextMoveToPoint(currentContext,0.0f,yRowTop);
            CGContextAddLineToPoint(currentContext,NSMaxX(self.bounds), yRowTop);
            CGContextStrokePath(currentContext);
    
            yStart += self.rowHeight;
            rowIndex++;
        }
    }
    

    When all is said and done, we get nice little fake tableview cell rows on the top and bottom of our clipview that looks a little like this:

    enter image description here

    The full subclass can be found here.

    you can use

    - (void)setUsesAlternatingRowBackgroundColors:(BOOL)useAlternatingRowColors

    with useAlternatingRowColors YES to specify standard alternating row colors for the background, NO to specify a solid color.

    I found that you can draw both the top and bottom part in drawBackgroundInClipRect — essentially in the missing else clause of @CodaFi’s solution.

    So here’s an approach in Swift, assuming you have access to backgroundColor and alternateBackgroundColor:

    override func drawBackground(inClipRect clipRect: NSRect) {
    
        // I didn't find leaving this out changed appearance at all unlike
        // CodaFi stated.
        super.drawBackground(inClipRect: clipRect)
    
        guard usesAlternatingRowBackgroundColors else { return }
    
        drawTopAlternatingBackground(inClipRect: clipRect)
        drawBottomAlternatingBackground(inClipRect: clipRect)
    }
    
    fileprivate func drawTopAlternatingBackground(inClipRect clipRect: NSRect) {
    
        guard clipRect.origin.y < 0 else { return }
    
        let backgroundColor = self.backgroundColor
        let alternateColor = self.alternateBackgroundColor
    
        let rectHeight = rowHeight + intercellSpacing.height
        let minY = NSMinY(clipRect)
        var row = 0
    
        while true {
    
            if row % 2 == 0 {
                backgroundColor.setFill()
            } else {
                alternateColor.setFill()
            }
    
            let rowRect = NSRect(
                x: 0,
                y: (rectHeight * CGFloat(row) - rectHeight),
                width: NSMaxX(clipRect),
                height: rectHeight)
            NSRectFill(rowRect)
            drawBezel(inRect: rowRect)
    
            if rowRect.origin.y < minY { break }
    
            row -= 1
        }
    }
    
    fileprivate func drawBottomAlternatingBackground(inClipRect clipRect: NSRect) {
    
        let backgroundColor = self.backgroundColor
        let alternateColor = self.alternateBackgroundColor
    
        let rectHeight = rowHeight + intercellSpacing.height
        let maxY = NSMaxY(clipRect)
        var row = rows(in: clipRect).location
    
        while true {
    
            if row % 2 == 1 {
                backgroundColor.setFill()
            } else {
                alternateColor.setFill()
            }
    
            let rowRect = NSRect(
                x: 0,
                y: (rectHeight * CGFloat(row)),
                width: NSMaxX(clipRect),
                height: rectHeight)
            NSRectFill(rowRect)
            drawBezel(inRect: rowRect)
    
            if rowRect.origin.y > maxY { break }
    
            row += 1
        }
    }
    
    func drawBezel(inRect rect: NSRect) {
    
        let topLine = NSRect(x: 0, y: NSMaxY(rect) - 1, width: NSWidth(rect), height: 1)
        NSColor(calibratedWhite: 0.912, alpha: 1).set()
        NSRectFill(topLine)
    
        let bottomLine = NSRect(x: 0, y: NSMinY(rect)   , width: NSWidth(rect), height: 1)
        NSColor(calibratedRed:0.961, green:0.970, blue:0.985, alpha:1).set()
        NSRectFill(bottomLine)
    }
    

    And in case you don’t draw in a NSTableRowView subclass:

    override func drawRow(_ row: Int, clipRect: NSRect) {
    
        let rowRect = rect(ofRow: row)
        let color = row % 2 == 0 ? self.backgroundColor : self.alternateBackgroundColor
        color.setFill()
        NSRectFill(rowRect)
        drawBezel(inRect: rowRect)
    }