Goodnotes everywhere

For the last two years, the Goodnotes engineering team has been working on a project to bring the successful iPad notetaking app to other platforms. This case study covers how the 2022 iPad app of the year got to web, ChromeOS, Android, and Windows powered by web technologies and WebAssembly reusing the same Swift code the team has been working on for more than ten years.


#Why Goodnotes came to web, Android, and Windows

In 2021 Goodnotes was only available as an app for iOS and iPad. The engineering team at Goodnotes accepted a huge technical challenge: creating a new version of Goodnotes but for additional operating systems and platforms. The product should be fully compatible with, and render the same notes as the iOS application. Any note taken on top of a PDF, or any image attached should be equivalent and show the same strokes the iOS app shows. Any stroke added should be equivalent to the one iOS users can create, independent from the tool the user was using—for example, pen, highlighter, fountain pen, shapes, or eraser.

img-1

Based on the requirements and the engineering team’s experience, the team quickly concluded that reusing the Swift codebase would be the best course of action, given that it was already written and well-tested over many years. But why not just port the already existing iOS/iPad application to another platform or technology like Flutter or Compose Multiplatform? To move to a new platform would involve a rewrite of Goodnotes. Doing so might start a development race between the already-implemented iOS application and a to-be-built from zero new application, or involve stopping new development on the existing application while the new codebase caught up. If Goodnotes could reuse the Swift code, the team could benefit from new features implemented by the iOS team while the cross-platform team was working on the app fundamentals and reach feature parity.

The product had already solved a number of interesting challenges for iOS to add features like:

All of them would be far easier to implement for other platforms if the engineering team could get the iOS codebase already working for iOS and iPad applications and execute it as part of a project Goodnotes could ship as Windows, Android, or web applications.


#Goodnotes’ tech stack

Fortunately, there was a way to reuse the existing Swift code on the web—WebAssembly (Wasm). Goodnotes built a prototype using Wasm with the open source and community-maintained project SwiftWasm. With SwiftWasm the Goodnotes team could generate a Wasm binary using all the Swift code already implemented. This binary could be included in a web page shipped as a Progressive Web Application for Android, Windows, ChromeOS, and every other operating system.

img-2

The aim was to release Goodnotes as a PWA, and to be able to list it on every platform’s store. In addition to Swift, the programming language already used for iOS, and WebAssembly used to execute Swift code on the web, the project used the following technologies:


#Why use Wasm and the web?

Even though Wasm is not officially supported by Apple, the following reasons are why the Goodnotes engineering team felt this approach was the best decision:

The reuse of more than 100 thousand lines of code, and of the business logic implementing our rendering pipeline was fundamental. At the same time, making the Swift code compatible with other toolchains lets them reuse this code in different platforms in the future if needed.


#Iterative product development

The team took an iterative approach in order to get something to users as quickly as possible. Goodnotes began with a read-only version of the product where users could get any shared document and read it from any platform. Just with a link, they would be able to access and read the same notes they wrote from their iPad. The next phase added in editing features, to make the cross-platform versions equivalent to the iOS one.

The first version of the read-only product took six months to develop, the following nine months were dedicated to the first bunch of editing features and the UI screen where you can check all the documents you created or somebody shared with you. In addition, new features of the iOS platform were easy to port to the cross-platform project thanks to the SwiftWasm Toolchain. As an example, a new type of pen was created and easily implemented cross-platform by reusing thousands of lines of code.

Building this project was an incredible experience, and Goodnotes has learned a lot from it. That’s why the following sections will focus on interesting technical points about web development and the usage of WebAssembly and languages like Swift.


#Initial obstacles

Working on this project was super challenging from many different points of view. The first obstacle the team found was related to the SwiftWasm toolchain. The toolchain was a huge enabler for the team, but not all iOS code was compatible with Wasm. For example, code related to IO or UI—like the implementation of views, API clients, or access to the database was not reusable, so the team needed to start refactoring specific parts of the app to be able to reuse them from the cross-platform solution. Most of the PRs the team created were refactors to abstract dependencies so the team could later replace them using dependency injection or other similar strategies. The iOS code originally mixed raw business logic that could be implemented in Wasm with code responsible for input/output and user interface that couldn’t be implemented in Wasm because Wasm doesn’t support either. So IO and UI code needed to be reimplemented in TypeScript once the Swift business logic was ready to be reused between platforms.


#Performance problems solved

Once Goodnotes started working on the editor, the team identified some problems with the editing experience, and challenging technology constraints got into our roadmap. The first problem was related to performance. JavaScript is a single-threaded language. This means it has one call stack and one memory heap. It executes code in order and must finish executing a piece of code before moving on to the next. It’s synchronous, but at times that can be harmful. For example, if a function takes a while to execute or has to wait on something, it freezes everything up in the meantime. And this is exactly what the engineers had to solve. Evaluating some specific paths in our codebase related to the rendering layer or other complex algorithms was a problem for the team, because these algorithms were synchronous, and executing them was blocking the main thread. The Goodnotes team rewrote them to make them faster, and refactored some of them to make them asynchronous. They also introduced a yield strategy so the app could stop the algorithm execution and continue it later, letting the browser update the UI and avoid dropping frames. This was not a problem for the iOS application because it can use threads and evaluate these algorithms in the background while the main iOS thread updates the user interface.

Another solution the engineering team had to solve was migrating a UI based on HTML elements attached to the DOM, to a document UI based on a full-screen canvas. The project started showing all the notes and content related to a document as part of the DOM structure using HTML elements as any other web page would do, but at some point migrated to a full-screen canvas to improve performance on low-end devices by reducing the time the browser is working on DOM updates.

The following changes were identified by the engineering team as things that could have reduced some of the issues encountered, had they done these at the beginning of the project.


#The text editor

Another interesting problem was related to one specific tool, the text editor. The iOS implementation for this tool is based on NSAttributedString, a small toolset using RTF under the hood. However, this implementation is not compatible with SwiftWasm so the cross-platform team was forced to first create a custom parser based on the RTF grammar and later implement the editing experience by transforming RTF into HTML and vice versa. Meanwhile, the iOS team started working on the new implementation for this tool replacing the usage of RTF with a custom model so the app can represent styled text in a friendly way for all the platforms sharing the same Swift code.

img-3

This challenge was one of the most interesting points in the project roadmap because it was solved iteratively based on the user’s needs. It was an engineering problem solved using a user-focused approach where the team needed to rewrite part of the code to be able to render text so they enabled text editing in a second release.


#Conclusions

Building a web project using a complex tech stack while working on a product full of challenges is an incredible experience. It’s going to be hard, but totally worth it. Goodnotes could never have released a version for Windows, Android, ChromeOS, and web while working on new features for the iOS application without using this approach. Thanks to this tech stack and Goodnotes’ engineering team, Goodnotes is now everywhere, and the team is ready to continue working on the next challenges! If you want to know more about this project, you can watch a talk the Goodnotes team gave at NSSpain 2023. Be sure to give Goodnotes for web a try!