Building Dispo

team.png

The past year at Dispo has been the craziest year of my life. In this blog post I reflect on the technical challenges in building the iOS app and the experience of going from 0 to millions.

Before joining Dispo, I spent a year working at Interlace in Brooklyn. Interlace, when I joined, was a video dating app where you get to see a bit more personality behind the person you’re matching with. One month into the job we made a hard pivot, throwing away the only app I had worked on in the App Store. While initially stressful, I got more comfortable with throwing away ideas if they aren’t working. Over the next year I got to work on 5 different consumer social apps, learning so much about startup life, chasing product market fit, and building products from scratch. With each iteration of the product, we had the opportunity to throw away old ideas and architect things in more efficient ways.

One day while scrolling on Twitter, I came across a tweet that changed my life…

When I saw the news break on TubeFilter that my friend Bhoka was joining a startup with David Dobrik heading marketing and Regy on engineering, I knew I had to see if I could get involved. I didn’t know Regy at this point, but knew through Twitter that he was a smart guy with great memes. There was a huge opportunity here.

One of the biggest difficulties I saw in building consumer social apps over the previous year was getting people to use the app. Knowing David Dobrik would be able to get millions of people hyped about a new product made me excited for the possibility of combining tech and Hollywood in the form of Dispo. The vibe of the product, the growing dissatisfaction with Instagram, and David’s fans would set us up for success.

Zero → Launch

dispopics.jpeg

Starting from Scratch

When I joined, there was already a Dispo app in the App Store with people using it every day. It seemed very simple — take photos and see them at 9am — but the codebase supporting it was a mess. It was built out by a contractor team months before I joined and wasn’t supported at all. Instead of trying to work around Dispo v1’s quirks, I decided early to start a new Xcode project.

A few of the weird behaviors of Dispo v1 included

  • All photos were taken on the main thread, meaning you had to wait sometimes three seconds before the UI was responsive and you could take another photo
  • The list of photos was stored in UserDefaults. For those unfamiliar with iOS development, UserDefaults is not a good place to store important and frequently changed data. It can be edited without a jailbroken phone, putting the app into an invalid state, and is at risk for being out of sync or losing the data entirely
  • Instead of using a standard UNIX timestamp, the photos were stored referencing how many seconds had passed since January 1, 2001
  • Viewing your photo library loaded every image you had taken into memory. For anyone with more than a few dozen photos, this made the app unusable and it would frequently crash. This happened primarily to the power users of the app, which was not a great reward for using the product

The only thing we wanted to keep from this app was the aesthetic of the filtered photo and the 9am develop time. When someone would eventually update to Dispo v2, they should expect to get a photo that looks similar to the original filter. More on the filter system and dealing with the quirks listed above later…

Architecture

Working on 5 different consumer social apps at Interlace gave me a great perspective on understanding what works and doesn’t work in an app’s architecture at a fast-paced startup. With each pivot, we refined the architecture for better performance, developer experience, and simplicity.

When starting the Dispo codebase, I wanted it to be highly modular and based on a foundation of reactive functional programming.

Reactive Programming

I chose to use a MVVM architecture with the new reactive programming framework Combine from Apple. This is what I was most familiar with (though normally with RxSwift instead of Combine), and also helped us recruit engineers as we scaled the iOS team. Reactive programming is fairly popular so I knew we would be able to hire great engineers.

Our View Models are just functions with the inputs being a bunch of publishers and the outputs being a bunch of publishers. This followed the architecture in the open source Kickstarter codebase. There is a simplified View Model below, but if you want to learn more about the reasoning behind this, Stephen Celis had a great talk at Functional Swift Conference.

func someFeatureViewModel(
  backButtonTapped: AnyPublisher<Void, Never>,
  pictureTaken: AnyPublisher<UIImage, Never>
) -> (
  dismiss: AnyPublisher<Void, Never>,
  saveProcessedImage: AnyPublisher<ImageSaveData, Never>
) {
  let dismiss = backButtonTapped

  let saveProcessedImage = pictureTaken
    .map { /* process image */ }

  return (
    dismiss: dismiss,
    saveProcessedImage: saveProcessedImage
  )
}

This View Model setup allowed us to keep a very consistent structure throughout the app. Any engineer that would join could jump into any screen on the app and know where to look and how to change the feature. In the old world of MVC architecture, there can be ambiguity as to where to put code and where responsibility lies, but this approach had clear lines of what should be in a View Model and what should be in a View Controller. Additionally, this approach is extremely testable — mock any dependencies you have, feed events into the input publishers, and expect the output publishers to behave in a certain way. We were set to build anything.

Modularization!

From the jump I wanted to make Dispo’s codebase highly modular. I had seen the benefits to this approach before, and with the tooling around Swift Package Manager (SPM) maturing, it was the perfect time to get into it. Modularizing would allow us to keep individual features separate from one-another, which would pay off as we needed to improve or throw away code quickly pre-launch.

Our modular approach generally followed this pattern: use a shared library for handling dependencies and commonly used code, and implement features in their own modules depending on this shared library. The shared library handled routing, notifications of certain events for other screens to act on, and housing our dependencies used throughout the app (Snapchat login & sharing, for example). In some cases, I would break out of this pattern if some module needed to be used by a few other modules but not the rest of the app. This helped keep build times as quick as possible.

modular.png

Modularity in practice unlocks a new wave of productivity. When working on a feature, I could build that feature in isolation (and even run it independently from the rest of the app!) and not have to wait for every line of code in the app to build. This was particularly helpful when we eventually changed our camera layout system. The original version was extremely versatile, but with the proper constraints in place we could simplify the logic on the client and make the design hand-off process much faster. I started refactoring just the CameraLayout module and created a single-purpose app to load only this module for testing. The main Dispo app wasn’t building at the time, but I could see my progress on the layout-only app. When it was ready to go, I then worked on getting our shared library compiling with these new breaking changes. Once that was done, I fixed the errors in CameraViewController and was finished. If the codebase wasn’t modular, this process would have taken much longer, as I would have had to deal with fixing code in other places in the app, even if it didn’t matter to the problem at hand — getting the new layout system working.

Throughout the rest of the year as we scaled up the iOS team, this modular architecture helped us move quickly and focus on the important work. Instead of fixing merge conflicts and dealing with namespace collisions, we could merge in code and get back to building.

Cameras

ogcam.png

Since the inception of Dispo v2, there was always an idea to have multiple cameras from launch day. With this in mind, I created a camera layout and filtering system that could be driven by server-provided data. This meant if we wanted to change the filter or tweak the design of any existing camera, we could do so without needing to update the app.

Filter system

The filter system in Dispo v1 consisted of a few steps: blur, vignette, grain, and a color filter. In order to support multiple cameras with arbitrary filters, I needed to make this data provided by the server. This consisted of a JSON config sent to the client, and in the case of the color filter, a LUT file to be used by our CIFilter pipeline. While building out the CIFilter pipeline I was able to lean on the iOS developer community and got some help from someone with the username cifilter! We later increased the capabilities of the filter system, but for launch this let us support multiple cameras without any client updates.

Layout system

In the early days, we couldn’t decide on any particular constraints for the camera layout — anything goes. For this reason, the layout system I designed needed to handle any configuration the following elements sent post-app install: images, buttons, and the viewfinder. I spoke to some friends about server-driven layout, but most of the advice was to have very specific constraints: a text label, a spacer, a text input, a button, all in a list. This wouldn’t work for us because we weren’t dealing with a list.

The initial layout system ended up being a combination of AutoLayout constraints and different button states and behaviors serialized into a JSON payload. I started implementing it wondering if it was even going to work, but it did! We could place images and buttons and the viewfinder anywhere on the phone’s screen using the power of AutoLayout. After we launched, we decided on more specific constraints for what a camera should look like and were able to simplify the server-driven camera layout system.

Updating from Dispo v1

In Dispo v1, all photos you took were stored locally on your phone. With v2, we wanted to back up the images to the cloud and let you share them with your friends. Some power users of v1 had thousands of images, so my main concern was making sure they didn’t lose any images when updating to v2.

During our TestFlight beta, I decided to use a different bundle ID so installing the beta wouldn’t overwrite your existing App Store Dispo app. I wasn’t yet confident about the migration process and wanted to be sure we wouldn’t lose anyone’s photos.

I began writing a system that migrated the old images from where they were stored in v1 to a temporary folder that would upload to the current account. One edge case I worked around was what if somebody logged into v2 with their friends account, but then actually wanted to import their photos to their own account?” To get around this, if someone had photos from v1, we placed a button inside their library to import the photos to the current account they were logged in to.

I tested the process manually dozens of times. Delete Dispo, install Dispo v1, take some photos, install Dispo v2, import those photos to the new account. Even without encountering any issues, I was still nervous going into launch. I had some friends with v1 install v2 as well, still no problems. My anxiety only went away a week or so after public launch when we had zero reports of people losing their photos. Phew.

Finally, when we released the app, we launched with a waitlist. This was needed as we scaled from 0 to tens of thousands to millions. That said, if someone had used v1 and got put into a waitlist, they’d be understandably upset. To prevent this, we used the same mechanism to see if they had taken any photos in v1 and let them bypass the waitlist.

Launch

launch.png

Launching Dispo happened very slowly then all at once. At a certain point, we had around 100 people using the TestFlight daily but we had no sense for how the app would scale with more people. We decided to go wider with the beta, and then everything happened…

Over the next day or two we filled up the TestFlight maximum cap of 10,000 people. It was incredibly exciting to see our friends and people from all around the world use the app we’d been grinding on for the past 6 months. At the same time, it still needed some polish. Luckily for me, the iOS bugs were out of the main paths of the app. Taking and viewing photos worked fine, so my work for the next few weeks consisted of improving the experience, fixing some bugs, and adding some missing states. Regy, on the other hand, was hit with issues left and right — the bulk of scaling an app from 0 to millions of people falls on keeping the servers up and running quickly. Regy was a superstar — juggling feature work, bug fixes, and scaling the infrastructure. One of my favorite memes was when Regy accidentally deployed some bad code which exposed an internal error (we hadn’t gotten there yet).

Overall the launch was really great. 6 months of grinding with Regy paid off, we made it! My time post-TestFlight was consumed with last minute features and fixes, as well as getting Japanese translation working for the community in Japan that sprung up during the beta. Overall the app wasn’t crashing and the main functions were working as intended. Within a few days of launching to the App Store, over a million people had used the app.

Closing Thoughts

Building Dispo from scratch was an incredible experience, from the technical challenges to all the friends I made along the way. It was thrilling to work on a product used by so many and I am extremely fortunate to have gotten to know so many amazing people.

If you are working on early products I’d love to be in touch and see how I can help. My DMs are open on Twitter!

Thank you to Lisa, Paari, Gonzalo, Pim, Point-Free, the Dispo team, and so many other friends :)

October 21, 2021