Mimicking the experience and engagement of browsing a physical wall of new sneakers, Big Spaceship took a stab at re-envisioning the way customers could explore Finish Line’s product catalogue on their phones. The goal was to ignite curiosity and encourage serendipitous discovery.

I was the tech lead on the project, which included coding and designing the majority of all core application code as well as planning and managing the project internally, overseeing two other technologists. Although this was my first iOS project I made an effort to leverage as many hidden features as possible to bring the interface to life.

Responsibilities

  • Tech Lead
  • iOS Lead
  • Prototyping
  • Project Planning

Technologies

  • Objective-C
  • UI Kit
  • PHP

Challenges

A core part of pitching the idea to the client was prototyping the visual experience. Being my first ever iOS project, I spent two weeks reading through books and forums to get a gauge on what’s possible. It was an exciting challenge to learn something completely new and do an immediate deep-dive, looking under the hood and leveraging every last ounce of what iOS and UIKit had to offer at the time.

Prototyping, Performance & CADisplayLink

To get a good understanding of the limitations of both hardware and software, I wrote the first prototypes in pure Objective-C for UIKit and without any visual helper libraries like Cocos2D.

The biggest challenge was getting a lot of product images to animate smoothly and react seamlessly to user-interaction. I had a hard time getting animations to perform well with NSTimer and it didn’t seem like people had done a lot of work with animating UIKit components beyond the typical A to Z transition.

While digging through the documentation I finally found the clue I needed: CADisplayLink. Instead of setting up an arbitrary timer, CADisplayLink plugs right into the display refresh rate – just like requestAnimationFrame in JavaScript – and animations were buttery smooth again.

// start
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                         selector:@selector(update:)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

// pause
displayLink.paused = YES;

// stop
[displayLink invalidate];

Since information about CADisplayLink was so hard to find in the community at the time, I subsequently wrote an article on the topic: http://www.bigspaceship.com/ios-animation-intervals/.

Building an Animation Engine on top of UIKit

With the main animation loop figured out, I focused on supporting many different visualizations of the same products and transitioning between modes seamlessly.

Ultimately, I settled on a structure that would be powered by a central animation engine and support combinations of different visualization behaviors and layouts.

Abstraction

All layouts and transitions would conform to common protocols and attach their own logic and data. That abstraction made transitions easily interchangeable and separating concerns so each class would do one specific task and allowed for three different technologists prototyping animations concurrently. Below is an example of how that applied to layouts:

FLVisualizationLayout.h

@protocol FLVisualizationTransition <NSObject>
- (void)animateInParticles:(NSArray *)particles
                        to:(CGPoint)origin
            withCompletion:(FLVisualizationCompletionBlock)completion;
- (void)animateOutParticles:(NSArray *)particles
                       from:(CGPoint)origin
             withCompletion:(FLVisualizationCompletionBlock)completion;
@end

FLBubblingTransition.h

#import "FLVisualizationBehavior.h"

@interface FLBubblingTransition : NSObject <FLVisualizationTransition>

@property (nonatomic, assign) CGFloat minScale;
@property (nonatomic, assign) CGFloat maxScale;
@property (nonatomic, assign) NSTimeInterval durationIn;
@property (nonatomic, assign) NSTimeInterval durationOut;

@end

Sorting by Colors

One unexpected hurdle was that colors on all products were vendor specific, meaning that a black Nike sneaker might have a color code of “BLK”, while a black Adidas sneaker could have a code of “space-black”.

This made it difficult to sort products by color, but after some initial user-testing, it became clear that this would be a feature too valuable to give up on. I set out to explore ways to programmatically get the color of each product.

Part of building the app was setting up a basic API that would buffer data between the e-commerce platform and the app’s own ecosystem. The API would regularly synchronize its catalogue, which proved to be a great spot to analyze product images and attach meta data.

Since the synchronization ran on its own ec2-instance, performance wasn’t too big of an issue, which meant I could simply use GD lib to analyze the images.

The first naive implementation simply looked at pixels in RGB space, quantized all red, green and blue values, stored their frequency in a list and used the most common value as the dominant color.

This created two problems:

  1. Colors with the same hue but different degrees of saturation would be considered as different colors
  2. The most common colors were gray tones – there was no way to prioritize saturated colors easily

To solve this problem I switched from RGB to HSV, which allowed me to look at colors independently of brightness and to give priority to vivid colors:

/**
 * Gets prominent colors as HSV. More saturated colors take precedence.
 *
 * @param  Image   $image              The image reference
 * @param  Array   $size               array(width,height)
 * @param  integer $numColors          How many colors should be returned
 * @param  float   $hueSampleSize      The smaller this value, the more
 *                                     precisely the hue will be compared
 * @param  float   $svSampleSize       The smaller this value, the more
 *                                     precisely the saturation and brightness
 *                                     will be compared
 * @param  integer $saturationPriority Higher values result in more saturated
 *                                     colors being prefered
 * @param  integer $granularity        How many pixels are measured per axis
 *                                     (1=every pixel, 2=every two pixels)
 * @return Array                       Array containing json-encoded hsv
 *                                     arrays: array("[h,s,v]", "[h,s,v]", ...)
 */
function getDominantHSV($image, $size, $numColors = 3, $hueSampleSize = 0.01,
                        $svSampleSize = 0.2, $saturationPriority = 4,
                        $granularity = 4) {

    $granularity = max(1, abs((int)$granularity));
    $colors = array();

    for ($x = 0; $x < $size[0]; $x += $granularity) {
        for ($y = 0; $y < $size[1]; $y += $granularity) {

            $ARGB = imagecolorat($image, $x, $y);
            $a = ($ARGB >> 24) & 0xFF;

            // bb: skip transparent pixels (in php, opacity
            // ranges from 0 = opaque to 127 = transparent)
            if ($a > 120) {
                continue;
            }

            $r = ($ARGB >> 16) & 0xFF;
            $g = ($ARGB >> 8) & 0xFF;
            $b = $ARGB & 0xFF;

            $HSV = $this->RGBToHSV($r, $g, $b);
            $h = $HSV[0];
            $s = $HSV[1];
            $v = $HSV[2];

            $quantizedH = round($h / $hueSampleSize) * $hueSampleSize;
            $quantizedS = round($s / $svSampleSize) * $svSampleSize;
            $quantizedV = round($v / $svSampleSize) * $svSampleSize;

            $hsvKey = sprintf('[%f,%f,%f]', $quantizedH, $quantizedS, $quantizedV);

            if (!array_key_exists($hsvKey, $colors)) {
                $colors[$hsvKey] = 0;
            }
            $colors[$hsvKey] += 1 + $saturationPriority * $s;
        }
    }

    arsort($colors);
    return array_slice(array_keys($colors), 0, $numColors);
}

Since there was such a drastic difference in patterns and styles, a lot of time was spent on fine-tuning the parameters so the script would yield the best results for the wide spectrum of sneakers. In order to preview the settings, I created a quick overview page that would generate all color codes and show the three most dominant colors on the fly:

In the app, I ended up sorting all colors by hue primarily and by brightness or saturation for dark products or products with low saturation.

- (NSComparisonResult)compareByHSV:(FLProduct *)otherProduct {

    float brightness = [_dominantHSVList[2] floatValue];
    float otherBrightness = [otherProduct.dominantHSVList[2] floatValue];
    float minBrightness = 0.33;

    if (brightness < minBrightness || otherBrightness < minBrightness) {
        if (brightness > otherBrightness) {
            return NSOrderedAscending;
        }
        if (brightness < otherBrightness) {
            return NSOrderedDescending;
        }
    }

    float saturation = [_dominantHSVList[1] floatValue];
    float otherSaturation = [otherProduct.dominantHSVList[1] floatValue];
    float minSaturation = 0.125;

    if (saturation < minSaturation || otherSaturation < minSaturation) {
        if (saturation > otherSaturation) {
            return NSOrderedAscending;
        }
        if (saturation < otherSaturation) {
            return NSOrderedDescending;
        }
    }

    float hue = [_dominantHSVList[0] floatValue];
    float otherHue = [otherProduct.dominantHSVList[0] floatValue];

    if (hue > otherHue) {
        return NSOrderedAscending;
    }
    if (hue < otherHue) {
        return NSOrderedDescending;
    }

    return NSOrderedSame;
}

The final output was a smooth gradient based on fully custom color analysis and sorting.

Team

  • Dave Chau (Design)
  • Chris Matthews (Strategy)
  • Nooka Jones (Production)
  • Bruce Drummond (Tech)
  • Josh Hirsch (Tech)