Comedy Central iOS App: Learnings on the bleeding edge
Invest in the rewrite
There is no word more terrifying in the world of software engineering than “rewrite.” So, you can imagine the delight in the eyes of management when our tech team made a case to do just that.
But like pictures, emojis are sometimes worth a thousand words…
Mobile development moves fast. Every fall, developers face a new version of their operating system and new tools. It becomes more challenging to bring your ancient code base up to speed. Development teams find themselves fighting with deprecations rather than experimenting with new features.
Some of the outdated vendor code we support sits on top of Restkit and XML feeds. Some of it relies on NSURLConnection, a class deprecated in favor of NSURLSession in iOS 7. If you plan ahead and your app supports a good level of abstraction, you can mitigate issues like these. But that isn’t always the case.
We set a goal to build a scalable mobile app architecture that would suit our future needs. We took a hard look at our current catalog of legacy apps. We asked ourselves what works, what doesn’t and what works well enough to improve and reuse.
Like a lot of legacy software, feature upon feature had compounded. With that comes ever-increasing technical debt. Our front-end software was approaching critical mass. Our feeds architecture was suffering as well.
These major factors, combined with a new, modern and flexible API solidified our stance. We got the green light from stakeholders to start over. We worked with in-house API developers and product managers to create something future proof. Something to suit our our own technological, production and business needs.
To Swift or Not to Swift?
A week or so ago, an amusing thread on the Swift Evolution mailing list caught my eye. It pled in its subject line “Seriously! Freeze Swift For Two Years After Release 3.0 !”
We started our project with Swift 1.1. Currently, Swift 2.3 and 3.0 (aka “The Grand Renaming”) are in beta.
Each release has been painful. The language changes out underneath us often. Debugging and refactoring have become more difficult. And compiling times have increased due to the removal of native method currying.
But on the positive side, Swift reduces the number of files in a project. It is concise. Its type-system and generics prevent us from writing a lot of code. It is also a great partner for Functional Reactive Programming. The standard Swift library already provides much of what you need for it.
And, we can all agree that Swift is far easier on the eyes than its verbose predecessor, Objective-C.
Experiment with Modern Concepts & Frameworks
We also introduced Functional Reactive Programming into our code. For the most part, we used it within our data layer. And we employed Promises, a concept used in concurrent programming languages such as Scala.
PromiseKit is an implementation and collection of helper functions. It brings the concept of Promises to iOS and clarifies the intent of complex code. It makes it easy to manage sequential and/or parallel execution of units of work with a clean syntax. It is thread-safe and comes with nice utilities for concurrency management.
ReactiveCocoa is an implementation of Functional Reactive Programming for iOS. We used it for bindings between data and UI. And also for its streamlined abstraction of the underlying iOS eventing mechanisms. We chose ReactiveCocoa over RxSwift because team members had prior experience with it. Also, RxSwift was too new at the time.
When working with closures and adding listeners, memory management issues can occur with PromiseKit. We also found that PromiseKit can make a mess of the debug chain at times.
These concepts can have a bit of a steep learning curve but are well worth the investment in the long run.
Don’t depend too much on others
Every time we downloaded a new version of Xcode and attempted to build the app, we ran into issues. These were almost always beyond our control. The Swift Migrator tool works pretty well but more often than not, we would see an error like this:
You are only going to be able to move as Swiftly as the libraries you have chosen to integrate into your own codebase.
Libraries, such as PromiseKit and ReactiveCocoa have great communities built around them. Their teams resolved issues during Swift betas with quick updates. But this isn’t going to be the case with every third party you choose or your business chooses for you. If the decision is up to you, ask yourself if you need it
Also, you should limit the total number of libraries you include. We found a high number of dependencies to be a culprit in app startup times on older devices. Apple recommended that you only include up to six external dependencies.
Use the tools that work for you
Visual layout in Xcode’s Interface Builder and Storyboards continues to improve. Novice programmers can create simple, yet impressive applications and adaptive layouts with ease. That doesn’t mean it’s the right solution for everyone, though.
Storyboards can slow down iOS development. They take a toll on Xcode and are prone to merge conflicts. They can also entangle your flow, view and business logic in one location. This doesn’t scale well.
We set out to create an application where as much of the UI as possible is data driven and dynamic. Storyboards were not the right solution to achieve this. We used a library called PureLayout. We constructed our views and modules into reusable, dynamic puzzle pieces of code.
Know Apple’s Best Practices
Business requirements and other restrictions sometimes get in the way of doing things right. But you should be aware of Apple’s Best practices and follow them as often as possible.
Limit backwards compatibility. Apple’s rule of thumbs is to take the current shipping version and take the current version one back. Currently, that would be 9.3 to 8.4 and in the fall when they release iOS 10, you should only go back to 9.3.
Treat warnings as errors and fix all warnings before you ship. This has been available in Xcode for Objective-C and will be available for Swift as of Xcode 8.
Profile early and then profile often. The earlier you find a problem, the less time you will spend untangling it. Always profile with release builds on the device rather than the simulator. And profile on the oldest and slowest devices that your app will support.
Migrate to Asset Catalogs. Make sure you include image sizes for all the resolutions you want to support. Scaling 3x images down on an older device can create memory spikes. You still have to load the large image to scale it down. Apple gave a convincing pitch for using Asset Catalogs this year at WWDC. (Watch the “Improving Existing Apps with Modern Best Practices” session).
Move to TLS. Up until now, Apple has given developers the option to turn off App Transport Security. They are starting to enforce this. By the end of 2016, they will ask developers to provide explanations for any exceptions in their app.
Follow Your Own Best Practices
We built an app that we consider to be our best work to date. And we also came out of it with a slew of our own best practices that we will continue to follow moving forward.
Use the highest level abstractions available to you. Manage asynchrony with an abstraction such as Promises. Manage events, binding and streams with an abstraction like ReactiveCocoa. Break out core functionality, such as your data layer. Create a separation of function and responsibilities. Maintain the least state possible and make every view stateless in effect.
Be dynamic. Data feeds drive the majority of our app. This allows our producers more flexibility and control than ever before.
Follow Gitflow. Tag your release and lock the pod versions in your pod file before release. Take part in architectural design sessions before starting non-trivial tasks. Engage in a focused code review afterwards.
Adopt solid deployment tactics. Watch Crashlytics like a hawk and consider potential problems with care. Any crashes in your QA cycle have the potential to blow up when the app is in market. Verify over-installing the app to be sure to save user preferences when needed.
And of course, test.
Test and automate so you can innovate
MVVM lends itself well to a testable architecture. It’s easy to mock and stub objects. You can represent views in automated UI tests. And ViewModels individual methods for manipulation and presentation are also easy to test.
Our data and networking layer is currently under a large amount of test coverage. This ensures that data retrieval and parsing is stable and safe.
We created a suite of Python integration tests for backend services schema validation. We also created a basic automation suite which captures services for playback. Next, we will include a screenshot comparison of current builds and a last good known. And then use DVR replay to remove live data from the equation.
As we extract sections of the app, to make into reusable libraries and pods, we are creating more unit tests. The goal is to achieve high coverage for these components.
Finally, we are working on a CTO-to-Dev-Ops-friendly way to display our testing results. Then, we can rest assured that all is well and start playing around with iMessage extensions. And other fun things coming down the iOS pipeline.
Check out the brand new CC iOS App now on the Apple app store!