The Sneaker Contest Reimagined

GOAT’s Black Friday contest is the largest digital sneaker event of the year. We hosted our first Black Friday drawing in 2015, just a few months after we launched the GOAT app. We were the first to offer sneaker enthusiasts a second chance at securing the best sneakers of the year at retail price. We’re talking Yeezy Turtle Doves and Supreme x Air Jordan 5’s for less than a fifth of their market value.

Our inaugural Black Friday promotion is an event that largely helped jump-start the company’s growth to over 10 million registered users today. Every Black Friday since, we’ve looked for new ways to celebrate sneaker culture and give back to our community with a really unique experience.

MERGING THE PHYSICAL & DIGITAL REALMS

This year, we are celebrating greatness in moments, places and people who have inspired us. We wanted to create our most immersive and global experience yet, by merging the online and offline worlds, and introducing an element of exploration to engage our users in a completely innovative way.

For the first time, GOAT has introduced an interactive map to encourage users to explore 125 destinations and Greatest Of All Time moments around the world in a bid to win over 1,000 prizes.

From historic sites tied to the Greatest Of All Time athletes like Michael Jordan and Serena Williams, to milestones in technology like the launch of the first iPhone, users can digitally explore and learn about locations of greatness to earn tickets to win prizes. In this post, we will explore how we built this feature and our learnings along the way.

CHOOSING A MAP SERVICE

To integrate an interactive map into the GOAT Black Friday contest, we needed to find a mapping service that suited our needs. Our top three priorities when deciding between services were:

1. Customizable Branding / Styling
2. Proper Map Interactions & Beautiful Map Movements (Flyovers & Zooms)
3. Developer-Friendly API

Mapbox checked all these boxes and, by using their Maps SDK, we were able to keep a consistent and clear brand representation throughout the entire Black Friday experience.

BUILDING THE FEATURE

Step 1 — The Boilerplate
Boilerplate… Why always boilerplate?

Luckily, Mapbox has a wealth of documentation in their Getting Started section that will help you take care of all of that — from Setting Up the SDK to figuring out how to Cluster your Data Points. It may prove overwhelming in the beginning, but to get your basic map set up and displaying data, these are the three points I mentally reduced it to:

1. Add sources (Your GEOJson data — we’ll talk about this next)
2. Set images (if you want custom pin markers)
3. Add layers (map pins, heat layers, data point clusters, etc)

Step 2 — Formatting your location data into GeoJSON
For the GOAT Black Friday experience, we wanted to see less singular locations the further we zoomed out and to see more clustered locations.

Clustered vs Non-Clustered

However, the only current way to create these clusters is to read the locations from a GeoJSON file. Our REST API returns a standard JSON response so we had to convert that response into GeoJSON on the client and keep a local copy of it in the FileManager’s document directory.

Standard JSON Response
Converted GeoJSON Location (Note: Grabbing the ID is instrumental. More details on why in Fine-Tuning)

IMPORTANT: In your GeoJSON’s coordinates, longitude MUST come before latitude even though the historical standard is latitude before longitude. This post by Mac Wright is enlightening if you’d like to learn more about working with the GeoJSON format.

Step 3 — Building the UI
At GOAT, we generally prefer to build our own components vs using a 3rd-party library. Why?

1. It is way less code.
In this instance, we were able to keep our component to a single file vs importing an entire library.

2. It is way less unfamiliar code.
We have pretty thorough code-reviews here at GOAT but the nuances of code means there will always be a few code smells hidden away somewhere. By writing the components ourselves, we’ll at least know where those smells exist and we’ll also have the opportunity to fix them without being forced to make a PR to an external party that may or may not ever be reviewed.

3. No fear of “library abandonment”.
We can continue to use, modify, or fix w/o fear of the library growing stale and then most likely not cooperating with future Swift & Xcode versions.

So for our map component, we built a ContainerVC that houses two child ViewControllers — a BackgroundVC and a ForegroundVC. The ContainerVC facilitates communication between the two and controls ONLY UI-related tasks such as gesture interactions, animations & fades, and sliding height. This allows us to use this component in multiple places in the GOAT app such as the Black Friday Map and the Buy Bar.

(Current Usage: GOAT Black Friday Map & GOAT Buy Bar)

PROTIP: Our first step when building out this component was to use extremely basic ViewControllers that had different background colors so we could get the interactions & animations down. This way we wouldn’t accidently mix any data logic into it.

However, if time does not allow for it, here are 2 of the more popular libraries out there that do something similar:

https://github.com/52inc/Pulley
https://github.com/SCENEE/FloatingPanel

FINE-TUNING

At GOAT, we put a lot of effort into building premium experiences for our users.

That’s why we built features like GOAT Storage and GOAT Clean and why we completely rewrote & re-architected the iOS app from the ground up (which we will hopefully get to talk about in another post). Not only do our users expect more, but we, as engineers, expect more. So when it came to the GOAT Black Friday experience, we knew we had to up our game.

Centering the Map
A key area of focus for us was keeping the map centered at all times. Most map experiences don’t recenter when their ForegroundVC (bottom sheet) changes position but as the screenshots below tell you… one experience is clearly better than the other.

Again, luckily there’s an easy way to do this and it had nothing to do with converting screen points to coordinate points (even though that is one method that works but I wouldn’t recommend it — spoken from experience).

The setContentInset method is your best friend for this. We call it when:

  • performing a zoom animation by calling flyToCamera
  • Tapping the ForegroundVC’s Navigation Bar
  • Panning the ForegroundVC’s Navigation Bar

BUT… just setting that worked great when user tracking was turned off and the phone was stationary. Once a user started moving, all bets were off and the map would begin glitching out. We found out that what was happening was that regionDidChangeAnimated gets called as a user is moving and the map’s camera is readjusting itself. So every time it readjusts itself, the contentInset would get reset.

So you will have to also have to set the contentInset in the regionDidChangeAnimated delegate method.

BUT… then that introduced another bug: when tapping on different locations, the map would freeze mid-animation when trying to fly from one location to another. In this instance, we found out that the map was prematurely trying to set the contentInset while still in the middle of flying to another location resulting in choppy animation.

To solve for that, we guarded the animated property. See the comments in code for why:

TLDR; guard animated == false before setting the map’s contentInset

Map Interactions
Syncing a user’s interaction from the ForegroundVC (the bottom sheet) to the map is pretty straightforward but how about when syncing a user’s interaction with the map and displaying it in the bottom sheet? It would be pretty silly for it to work one-way but not the other.

This is where that ID in your GeoJSON’s properties comes into play. When tapping on a specific map-pin, you will first use the visibleFeaturesInRect method to determine where on the map you are tapping and if there is either a cluster or location there.

If there is a map-pin, the only way you will be able to identify which pin you have selected is by using the feature’s attributesForKey to access the ID you saved in your GeoJSON. It is pretty straightforward after that!

GOAT BLACK FRIDAY’S WORLDWIDE TAKEOVER

We launched our interactive map contest on November 9, and within one day, we surpassed 1 million participants. Our users have explored sites and stories around the world including the coordinates of the oldest known leather shoe (5,500 years old) in Armenia to Muhammad Ali’s historic victory over George Foreman in Democratic Republic of Congo. Some of these users have even visited these locations in person unlocking a shareable AR experience that superimposes a “Greatest Of All Time” graphic into the real world.

By incorporating technology in a completely unique way, we’re able to bridge the gap between the physical and digital realms with our most immersive and global experience yet.


GOAT’s Mobile Engineering team is hiring! 
Visit our careers page to learn more.