By 2016, the developers of Evernote for iOS were struggling to add new features to the app, keep it working with new versions of iOS, and untangle technical debt in the code. To fix this, we began an effort that ultimately produced an app with a new design, a new user interface, and a new software architecture—all delivered by a mostly new team.
The previous version of Evernote for iOS (7.x) was designed, built, and released alongside Apple’s iOS 7 in the fall of 2013. In the following years, it acquired millions of users and a lot of new features, but also a lot of technical debt. This burden was manageable until three things led us to reconsider the product from the ground up: we had a new design to implement, a new editor to integrate, and a newly staffed team ready to build a more sustainable app.
- Nearly everything about the user experience in the app was reimagined, to make Evernote faster and more simple to use. As a result, all of the existing user interface code would have to be replaced and rebuilt.
- Evernote’s editor had been rebuilt as a separate component, common across all of our products. While it had been successfully integrated into Evernote 7, the effort left us with a mess of clumsily connected code that wouldn’t be needed in an app rebuilt with the new editor in mind.
- Changes in the development team had left us with fewer people with deep knowledge of the existing code. We wanted to build something modern and sustainable instead of learning to maneuver around our technical debt.
Our iOS team had an engineering manager, a product manager, two designers, several QA people, and four developers. Although we knew rebuilding our app would be a big effort, we couldn’t have everyone on the team start working on it immediately. We had to continue to support our current version, so we began with one person exploring the rebuild and bringing issues back to the team for discussion.
The developers began with a retrospective look at the existing code and architecture. It can be difficult to step back from an app that you are intimately familiar with and emotionally invested in and ask “What would I do if I started it all over again?” But it can also be liberating, and after a few sessions where we reexamined the origin, design, and purpose of each piece, we were gleefully throwing stuff out and rethinking how to rebuild the app.
We also knew that how we made decisions as a team was going to be very important, as we were embarking on a rewrite with many thousands of important decisions to be made over the coming months.
Rather than have a single technical lead dictating strategy, the four developers decided to work together and found that they were in broad agreement about the outline, if not all the details. Where they disagreed or were unsure, we encouraged them to experiment and demonstrate the results. To move ahead, we had to tolerate some uncertainty but remained confident we could work our way out of trouble. We were collaborative, often passionate, yet always professional.
Having a rough plan also let us focus on the skills we wanted as we began interviewing and hiring additional people. We decided that we needed three additional iOS developers with experience working with UIKit to build native apps.
Armed with a new design, a clear business case for rebuilding the app, solid support from within the company, a code name (“Lightning”), and a team ready to go, we started defining some of the key technical requirements and constraints.
Modern UIKit: Since nearly all of our users run a recent version of iOS, we would be able to build our new user interface code using the latest UIKit features. This would include using auto-layout everywhere, using size classes, and support for split-screen multitasking on the iPad. To help ditch some old habits, we rewatched all the UIKit sessions from the previous couple of years at WWDC. We worked with our design team to think about iOS size classes and orientation instead of specific devices or screen sizes; we decided to define all of our auto-layout constraints directly in code for precise clarity and control; and we looked carefully at how massive ViewControllers tend to grow out of control and developed strategies to keep our new ones a reasonable size.
Swift 3.0: We wanted the new code for Lightning to be modern and sustainable, so we decided to write it with Swift 3.0. We had been writing small pieces of code in Swift since its introduction, and had transitioned them with each evolution of the language, so bringing our existing Swift 2.x code up to 3.0 only took a few days of fixing build errors and dealing with all the API renaming. As we wrote new code, we also absorbed many of the changes in the standard library, API design, and naming conventions so we could make things as “Swift-like” as possible.
A fresh start: While we had planned to write a lot of the new UI from scratch in Swift, we also wanted to preserve large chunks of the existing app. To do this, we decided to create a new Lightning project in Xcode and then determine what to bring into it from the existing project. We wanted to bring over code and assets from the previous app only after thoughtful consideration, so that the new app would only include things we were certain to use. Almost all of the old code was in Objective-C and so, with the new code in Swift, we would have a clear delineation between the two bodies of work.
Clean, clear, simple, bold: Instead of attempting to plan in detail all the pieces of software architecture we would need for Lightning, we embraced some values to guide their creation. We wanted classes that were clean, self-contained, single-purpose, and with few dependencies; classes, structs, enums, protocols, etc., would all have a clearly named and well-defined purpose—each piece would be simple and focused on doing one thing well. We wanted to create code that worked exactly the way we intended, without premature optimization or compromise. And so we began with these goals in mind while acknowledging that the real work would end up being messy, compromised, and complicated.
We started by standing up an early build of the new app that would simply connect to the Evernote service, get some data, and display a list of notes. Our objective was to quickly create a working, but very limited, application that we could then rapidly iterate on. To do this, we had to pull some basic pieces into place.
Evernote accounts: The first step when using Evernote is to sign into your account. We reused a lot of existing, well tested, Objective-C code for authentication and logging into accounts, but built new Swift code around it to have a clean presentation of an Evernote account for the rest of the app.
Data persistence and synchronization: We already had a successful Objective-C framework (CoreNote) for data persistence and synchronization with the Evernote service. CoreNote maintains the user’s data in a CoreData database and uses the Evernote service API to keep the data in sync with the user’s account. After some configuration work, we were able to connect the framework to an account and have it sync its data.
Data model: We built a data model with clean, simple representations of all the things in an Evernote account: notes, notebooks, resources (i.e., data attached to notes), etc. This data model would isolate and manage interactions with the persistence and synchronization framework, be independently testable, and present an API tailored to the needs of the view models.
Note list: The centerpiece of the user interface in Evernote is the list of notes in an account. Later we would be putting a lot of effort into the note list, but to start we simply created a UITableViewController with generic cells that displayed the titles for each note. The data for the table came from a note list View Model that handled taking info from the Data Model and formatting it correctly for the table view.
Putting these pieces in place demonstrated basic functionality and helped identify which portions of the existing code we could reuse. We also used them to outline an app architecture with simple but firm boundaries.
We knew that those boundary lines would be tested when we began adding more code. Whether by accident or temptation, it would be easy to have code in Lightning Data trying to reference things in the UI, or UI elements attempting to know about things in CoreNote’s persistence scheme. These groups of code were in the same target in the same Xcode project and could easily become tangled.
To combat this, we paused development on features and began organizing the code into a few frameworks: one each for CoreNote (our persistence and synchronization library), Lightning Data (which clearly separated our data model from the rest of the app), and Lightning UI (for some common UI pieces). The CoreNote library was already a framework in Evernote for Mac, so making it one for iOS was fairly simple, but the rest of this effort was tedious, spanning several weeks, and overlapped our conversion from Swift 2.3 to 3.0. The existing code had to be untangled, with the occasional puzzle over “Which framework does this piece belong in?” While creating the public API for the frameworks, we were also adjusting to Swift 3.0 and “the great renaming” that came along with it. For added excitement, most weeks also brought a new beta of Xcode and the iOS 10 SDK.
When the dust had settled and our build system could churn out the app reliably, we were then able to start building the pieces we wanted for our new app design.
Starting in Swift
We started work with a team of seven developers: four at our headquarters in Redwood City and three in Austin. A few had been tinkering with Swift since 1.0, but most were fairly new to the language. Because of the proliferation of resources for learning Swift, however, and the fact that we had a team of smart, curious people, we were confident we could start producing code without a steep learning curve.
As we began working together, we discovered that projects like this often progress through several phases: one is where you write all your Swift code much like you would write it in Objective-C. Another is where you watch the “Protocol Oriented Programming” video, try to use Swift protocols everywhere, then learn when to dial it back a bit. Finally, the ideas behind the “great renaming” that came with Swift 3.0 will settle in and you’ll start to name things in your code the same way; you’ll explore different ways to define and access properties and discover all their tradeoffs; you’ll find interesting edge cases and behavior with “bridging” and “unsafe” types; and you’ll start using unique and interesting features in the Swift standard library.
We asked each other, “Is there a style guide?” and settled on the common Swift community style used in most of Apple’s code, encouraged by Xcode, and loosely documented several places online; but we didn’t pour much passion into this—working code was more important. Eventually, we discovered the Swift-Lint tool and ran that over the code occasionally to clean things up.
There is much to explore with a new language, but we needed to be focused on our work so, everywhere we could, we borrowed existing tools and practices from the Swift community and resisted the urge to invent our own.
We were all learning together and used several tools to collaborate: Slack, meetings, in-person visits, Jira & Bitbucket, and various flawed flavors of video conferencing. We adapted the elements of Agile and Scrum that suited us: two-week sprints, team planning sessions, daily virtual stand-ups in Slack, and frequent longer sessions where developers could discuss issues. It was not a model process, and we have since become more disciplined, but it was enough structure to keep the team focused and provide a rough idea of our progress.
Bitbucket was especially important, not only for code review but also for the window it provided into each others’ developing style and technique—more than once, we reworked code after seeing what others had created in another part of the app. Review comments were professional, specific, and constructive. Occasionally something brewed up into a bigger issue to be taken up at our next developer meeting.
Allowing everyone space to blow off steam was very important. People who may be confident with Objective-C can feel unsettled when they find themselves in a new environment. Not only is Swift a different language, but many of the surrounding tools in Xcode don’t work as well—and sometimes not at all. Everyone has their own threshold where “inconvenient” becomes “intolerable,” so it’s good to be able to stop and vent to your colleagues from time to time. Ranting about developer tools is a fixture of the profession and a normal thing to do on your way to building something great.
Note list in action
The note list, a vertically scrolling collection of notes, is the centerpiece of the app’s UI. The list may show all your notes, notes from a specific notebook, or notes from a set of search results. Each element in the list is a “note preview,” with the note’s title, date, and a sample of text and images.
Like much of our user interface, it shows you a list of things and lets you act on them—this fits well with the classic model-view-controller scheme. Previously, our view controllers often became bloated and disorganized, so we added a view model to help organize and format data before handing it off to the views for display (Data Model -> ViewModel -> ViewController -> View). Here’s how this works in practice with our note list as an example:
When our NoteListViewController’s UITableView needs to produce a cell to display a note, it gets an instance of a Note from the data model. Then it creates a view model for that note called a NotePreview. The NotePreview looks at the properties of the note, in this case the date the note was last modified. Next, it uses some predefined rules and preference settings to format the data. Since the last modified date property is from the day before the current one, it formats it as “Yesterday” instead of a standard formatted date string.
The NotePreview and the destination NotePreviewView are unacquainted, so the NoteListViewController’s configurePreviewView method takes the formatted date string from the NotePreview and drops it in the NotePreviewView’s noteDataLabel. Similar operations fill in the rest of the note’s title and content.
This is a simple, slightly boring, standard way to organize iOS code and that’s exactly what we wanted. No one is going to write books or give conference talks about it, but we wanted our current people to be able to focus on building out the app’s features, and wanted future developers to find something simple and familiar.
Working with Design
An exciting part of writing all the new Swift code was bringing to life a great new design for our app. When working with our designers, we started with the idea that design and development is a conversation; there isn’t usually a point where the design is “done” and then some coding happens and then everyone is happy. Instead, developers write some code and show the designer the result—often in a working build but sometimes just a screenshot. Great work happens through iteration. Feedback ensues, then more coding, more feedback, etc.
New designs would often provoke a series of questions from developers: “Can we change this a bit, there is a built-in UIKit thing I can use,” “I think the touch-target for this thing is too small,” “I’ve never seen this before, it isn’t copied from our Android app is it!?!” “What happens when I rotate the device?” “There is another thing in the app that looks like this, can we make them the same?” “What does it do when I’m offline?” “What does it look like on larger screens?”
As a side note, this last question was originally, “What does it look like on iPad?” When talking with the designers, we had become (perhaps annoyingly) fanatical about referring to regular and compact screen sizes instead of “iPhone” or “iPad.” Partly this came from the trauma of attempting to get our old code to work nicely on the big iPad Pro and to support a split screen.
While you can test a lot of things by showing people pictures and prototypes, you can’t know if you have it right until you have a version of it working in the app. The work we put into having clear boundaries and narrow dependencies in the code paid off when we had to rewrite portions of the UI in response to user testing and other feedback. Because changing the app was now easier to do, it reduced the penalty if we didn’t get it exactly right the first time.
Collaboration and code review
While we had regular sessions where developers could discuss whatever they liked, a lot of important feedback happened in code review—most days there were six to ten new pull requests to review. We tried to keep the scope of these manageable and most were reviewed within a day, and when one would linger or seem too large, the author would usually back up and rework their PR. The most common review comments had to do with the names of things and requests for more clarity, readability, comments, and other documentation. Frequently, duplication was avoided when someone would point out similar code in another part of the app. Comments were professional, not overly pedantic, and focused on values (readability, testability, maintainability) rather than personal style. Occasionally, feedback and disagreement would expose larger issues with goals or architecture—those we would pull out of the code review process and sort out in our next developer team meeting.
While it was exciting to have lofty goals when rebuilding our app, we also knew that in the interests of time, compromise would occasionally be required. We had intended to rewrite all of the app’s user interface in Swift, but paused when we looked at our app’s settings:
There were many more screens of settings like these, all connected to the app in specific ways and written in fairly standard UIKit. While some minor redesign was needed, we couldn’t really think of a good reason not to reuse the existing code. Rewriting was high-cost and low-value so the old code stayed in. We had many good reasons to be doing all of our shiny new architecture and code but they were our own reasons. While rebuilding the app, we reminded ourselves that the only thing our users would care about would be their experience using it.
We wanted automated testing to be an integral part of our development process, but pausing development while we created a good framework for testing was too much of a short-term penalty. We had an excellent team doing manual testing and we relied on them for much of our first release. Now we’re working on expanding the scope of our tests and building them into the development of new features.
Often it’s when you’ve just finished writing some code that you realize how you might have done it better if you started over again. In those cases, we usually only stopped to rewrite something if it was foundational and would make other things better too. Because each developer was usually focused on adding a particular new feature, we sometimes missed opportunities for building common code. When we discovered these, we would have to add a “refactor and cleanup” story to our backlog and move on.
In an attempt to move development along faster, we added more developers to the team. Although often disastrous, this move actually worked well for us. The foundations of the app had already been built, we had several isolated components, and the new people were highly skilled, so this made a significant impact on our backlog.
Another (and usually more successful) way to speed things along is to reduce the scope of the work. Evernote for iOS had grown a lot of features over the years so we took a critical look at each one again. Anything that no longer made sense, was infrequently used, or didn’t fit the new design was cut. This allowed the team to focus on core features, quality, and polish.
There is a mythology about creating software that says it gets made by eccentric rock-star geniuses who madly code through long caffeine-fueled “death marches” and “crunch times.” To build Evernote 8.0, we instead used a team of skilled, experienced, and diverse professionals. We planned and estimated together, learned together, wrote a lot of code together, and stayed focused. There was serious pressure to deliver the app and debut our new design but, while we sustained several delays, everyone kept their cool.
You can test an app as much as you like but some things you won’t discover until you deliver it to millions of people. Bugs that seem rare and unreproducible in testing scale up to affect large numbers of users. Lower-priority issues turn out to have buried side-effects that are much more serious. After Evernote 8.0 appeared in the App Store, we started monitoring automated crash reports, app store reviews, social media, our support forums, and feedback to our customer service team.
We had some serious issues to address and immediately began a series of app updates. We had to build these updates quickly but with great care and focus so we didn’t inadvertently introduce new problems. Some issues we could fix right away but others required more investigation.
For example, some people were reporting a crash when starting the app—a problem we hadn’t seen in our testing and none of our beta testers had reported. We really needed access to someone’s device so we could reproduce and fix it, but finding someone who had this problem and would let us run a debug session on their phone was difficult. Luckily, we were discussing this when my brother called me to complain that our app update was crashing his phone, so I spent the afternoon in his office debugging our app startup code. The crash turned out to be a problem with interpreting some login data saved in a format that was used by an old version of the app. That version was only around for a couple months over two years ago and was a problem only if the user had remained signed in since then.
Our stream of minor updates continued as we found and fixed issues. The App Store’s dramatically improved app review time really helped us get these fixes in people’s hands quickly.
As things settled down, we were able to start addressing some issues people had complained about but which weren’t critical bug fixes. We reworked the note list to improve performance and included some minor UI changes that were nearly ready for the initial release, but hadn’t made the cut.
We next took a pause to get the team together to talk about what we had done and how we could improve things in the future. As a result, we refined our process for planning, estimating, communicating, and developing. Part of looking back was making an inventory of any technical debt we had created while getting the release out the door. We added that to our backlog as well so we could begin to address it in balance with new feature development.
Since we shipped, we’ve built a suite of automated tests that exercise the code both at the unit level and by driving the user interface; a growing number check performance as well as correctness. Rather than have someone on the test team in charge of test automation, we moved that person to the development team and made it everyone’s responsibility. The scope of our tests is still more shallow than we would like, but we have a framework to build upon and have started changing our habits, making sure the tests pass before merging code and building test creation into the scope of new work.
WWDC 2017 brought a new version of Swift and new iOS 11 betas to test against. By beta 3, Xcode 9 was able to build our existing Swift 3.2 code without modification. We created a Swift 4 branch, and after one developer spent a couple of days making adjustments, that was working too. Getting our tests working took longer because they use third-party code that also needed an update.
Now we have a slew of work lined up supporting new features we have planned for the app and new technology in iOS 11. We’re starting with an app that has a solid architecture, malleable features, and accessible code. All ready for a long game of supporting a modern, sustainable Evernote for iOS.