We started the ezCater iOS app in August 2016. We had a basic working API, an opportunity to build an app from scratch, and six engineers ready to do it. Right on time, four months later, we launched our first iOS app. Within hours, we had a brand new customer successfully place an order. We've just shipped version 1.3, and since launch, we've had only one crash. Swift has helped us to create a robust codebase which cleanly handles errors and edge cases. Our architecture has helped to keep our components small, reusable, and encapsulated.
We started fresh and had no legacy codebase to support, so the decision to choose Swift was an easy one. Apple is pushing hard on Swift adoption. Barring a few exceptions, it's usually a good choice to jump onboard when Apple takes this approach. It's already being used successfully in hundreds of production apps, small and large.
Swift has many important advantages over Objective-C, including:
- type safety
- protocol extensions
Personally, after spending the last year writing only Swift, I miss very little about Objective-C. Notice that I said "very little," and not "nothing."
Xcode still has a lot of trouble with Swift-only features (enums, generics, etc). SourceKit routinely crashes, syntax highlighting gets lost at least a few times a day, and Xcode has trouble cmd + clicking into code in shared frameworks. Also, we still don't have refactor support.
Given our project timeline constraints, we chose to delay our transition to Swift 3.0 until after we had shipped 1.0. We found a week where all but one member of our team would be off, made sure we had all of our branches merged, and sacrificed that lone engineer to the Swift 3.0 transition gods. It went rather smoothly, with little friction aside from some reworking of our networking logic that went along with the upgrade to Alamofire 4.0.
View models: everyone has their own flavor, and they seem to be increasing in popularity. They provide an easy place to separate out business logic from view controllers, and help reduce the severity of Massive View Controller syndrome. We used view models for precisely this purpose, to pull out networking, data source functions, data formatting, and more from overgrown view controllers. If the view model gets too big, it can be further separated into network controllers, data sources, and formatters. As we progressed through the project, we found a level of separation that worked for our team and kept our codebase readable and maintainable.
We developed a generic data source protocol, DataSource, to encapsulate the bulk of the logic that goes into displaying object-backed lists. It is UIKit-agnostic, but primarily designed to be used with UITableView and UICollectionView. We also extended it to easily handle multiple sections (SectionedDataSource) and NSFetchedResultController-backed lists (FetchedDataSource). The reusability of DataSource gave us a strong foundation for building all of our list-based screens.
During our initial planning meetings, we discovered some areas of our app that could benefit greatly from caching (caterer menus, recent orders, recent addresses). The quicker we can show our users the content they want, and the less users see loading indicators, the better.
After a bit of research, we decided to use Core Data for our persistence layer for a few big reasons:
- Supported by Apple
- Built-in integration with UIKit
- Abundance of documentation
Our Core Data stack consists of a single read-heavy main context and short-lived, write-heavy background contexts. Everything that gets displayed is read from the main context and everything that gets written into the backing NSPersistentStore goes through the background contexts. This keeps our main context (on our main queue) unblocked by writes from incoming data from the server.
When we get data back from our API requests, we spawn off a background context and toss it into a serial queue. Next, we parse the JSON into NSManagedObject model objects, create/update/delete from the background context, save to the NSPersistentStore, and then the background context is killed off. We listen for NSNotification.Name.NSManagedObjectContextDidSave on each of these background contexts, and merge their changes into our main context to ensure that it is always up to date.
We discovered one gotcha when using NSFetchedResultsController due to our stack setup, which is described in detail in this blog post from Black Pixel.
We make use of NSFetchedResultsControllers, in some shape or form, in almost every list-based screen. They always fetch and observe objects from the main context. Sometimes we connect them directly to our UITableViews and other times we use the delegate functions to watch for changes and construct a more customized data source.
Any time caching is implemented, data consistency is always a concern. Should you show potentially stale data to the user while waiting for the updated version? In order to easily and quickly handle some of these problems, we decided on an incremental approach to displaying cached data. We started by displaying almost no cached data. Since then we have been slowly, piece by piece, adding it to each section of the app.
Overall, we feel that Core Data was the right choice for us. It acts as a nice layer of separation between our models and views, provides a single source of truth to read and observe changes on our model objects, and easily gave us the ability to incrementally persist and display cached data.
Our network layer consists of multiple service classes, each containing groups of related requests. We have a service for caterers, a service for orders, a service for accounts, and so on. Each service interacts with a shared client, which is a thin wrapper around Alamofire's SessionManager. This gives us a lot flexibility, since our services don't know the internals of the shared client class. We could, theoretically, change our entire underlying networking implementation and it would only affect our shared client class.
The Storyboards vs XIBs vs programmatic layout debate is one of the most active in the iOS community. Each approach has pros and cons, and I think the right answer for one team may not be the right answer for another. Personally, I'm a huge proponent of programmatic layout, and I pushed hard to get everyone on board. We tested the waters for the first few weeks, found it worked for us, then jumped in head first. Here are a few of the main reasons we decided to use programmatic layout:
- NSLayoutAnchor completely changed the game and made layout code efficient and readable.
- The introduction of UIStackView was also a big win for layout in code. It drastically reduces the need to manually create constraints.
- We had a medium sized team, which would have caused some havoc with merge conflicts related to storyboards.
- Programmatic layout creates a single source of truth for all of our view configuration and layout, so you never have to wonder whether a particular piece of your layout is coming from code or Interface Builder.
- Very easy to create reusable layout code.
- Because you can use any view controller initializer, and not just the prescribed ones for working with Storyboards and XIBs, you can do compiler-enforced dependency injection through required parameters in view controller initializers.
- It's tough to review storyboard/XIB changes through GitHub. Programmatic layout enabled us to more easily verify consistent and robust layout code.
- It allowed us to gain a much better understanding of UIKit, the layout process, view controller lifecycle, and constraints.
We also found Anchorage, a lightweight and simple library that builds on NSLayoutAnchor to make our code even more readable.
First off, if you are unfamiliar with Cocoa Touch Frameworks, here is a great tutorial.
We decided to use a separate core framework for any potentially shareable code. This included our entire networking layer, entire persistence layer, most of our protocols and extensions, and most of our UI components. A big benefit that frameworks afforded us was the ability to reuse code in App Extensions. During an internal hackathon here at ezCater, we were able to quickly and easily create a Today extension for recent orders by reusing our networking and Core Data code. Frameworks also encouraged us to develop our components with friendly public APIs. It required us think about what parts of our components we wanted to expose, and helped us create more robust components that were useful in more than one scenario.
Overall, our team synced very well and the entire project went smoothly. We believe we did many things right, but we also learned a lot about where we can improve.
- Swift was a great choice. It definitely has a lot of room for improvement, but overall it helped us build a safe, robust, and concise codebase.
- Core Data enabled us to easily keep our data in sync across the app
- Our flavor of MVVM kept our UIViewControllers slim and provided a clean separation of business logic from our views.
Things to Improve
- Tests - We had none at launch and have been working hard to catch up. In the coming months, we hope to have a full suite of tests covering our models, network requests, JSON parsing, and view models.
- Logging - There are some app states that we don't expect users to ever be in, but when they do, we want to know about it. There are also some non-crashing errors that we'd like to track.
What does our technical roadmap look like for the future?
Coordinator objects let you pull your navigation logic out of your view controllers. We plan on implementing A/B testing in the near future, and coordinators would allow us to safely and quickly experiment by encapsulating navigation code. Interested in more info about coordinators? Here are some great resources:
- The Coordinator - Soroush Khanlou
- Coordinator Redux - Soroush Khanlou
- Navigation Coordinators - Bryan Irace
- Flow Controllers - Krzysztof Zablocki
We use some great open source software in our app including:
We plan on contributing back to the community and are working on identifying areas within our project that would be good candidates for open sourcing.