When we first delved into iOS development in 2014, we had an unexpected problem — there were no simple and efficient tools to work with vector graphics. The available native options had significant disadvantages:
- Combining UIViews was cumbersome and heavy on resources.
- Using CALayers was more straight-forward but even more resource-intensive (that’s why they are used mainly for animations).
- Drawing directly on canvas using CGContext was fast, but writing the code was a complex and slow process.
Third-party libraries were available (SpriteKit, iOS Charts, and SVGKit, to name a few), but most of them solved a single specific problem, and none provided a universal API for working with vector graphics.
We decided to leverage our experience in Desktop and Web technologies to create an easy-to-use multipurpose library that would work with SVG files efficiently, draw and animate vector images, create graphs and interactive custom widgets.
One of the best approaches to deal with custom graphics is to use a scene graph (a tree-like hierarchical representation of the graphical scene). That’s what we decided to do with Macaw. To have clear goals in mind, we outlined desired features and a set of rules we would adhere to.
The minimum feature set we wanted to support included a scene graph to compose graphics from primitives, an SVG parser and serializer, support for basic animation and touch events. As for the self-imposed rules, we settled on this list:
- Backward compatibility. In the iOS world, it’s almost expected to rewrite parts of your app whenever Apple releases major updates to their tools, and that can be very frustrating for the developer. We set a goal of creating an API that was well-designed from the beginning and could remain unchanged for years.
- Clean and self-explanatory API. Instead of copying many established but convoluted practices from complicated graphics APIs, we decided to rethink some of the vector graphics approaches to make them easier to understand.
- Code readability. Swift was designed with meticulous attention to code readability. So we’ve followed this practice and took care to make code easier to read and write and keep it consistent with the general style of iOS.
It took us ten months of development to release the first public version in October 2016. It included all the features we planned — a reactive scene graph, events, animation, and even SVG support.
Two weeks later, we had 1000+ GitHub stars and a growing understanding of what changes and improvements were needed. The biggest realization was that a lot of people simply needed to draw a static SVG without all the bells and whistles. Three months later (next major version), we included a simple view that rendered an SVG file. We also confirmed our suspicions that the SVG parser was the most crucial part of the project, and have worked on developing and improving it ever since.
Another thing we noticed was the demand for complex animations. In the following release, we included support for node animation that modifies the scene (called ‘content animation’ in Macaw). A bit later, we introduced morphing, path animations, performance enhancements for content animations, and other fixes and improvements.
After 5 years of development, Macaw has grown from a scene graph implementation for iOS to a serious vector graphics toolset available for iOS and macOS with support for:
- complex animations
- SVG parsing
- low-level touch API
- content layout
- clip and mask support
- patterns and effects
Naturally, a lot of problems came up during the development, and it’s not possible (or interesting) to list them all. Here are two that we are most proud of conquering.
Native vs. Content Animations
Initially, all of the animations were done with native iOS tools. It was fast but restrictive — no animation could include actions that require changing the state of the content graph. Turns out that complex animations often require it, so we had no choice but to add content animations.
Because redrawing the whole graph is costly, our implementation supports a partial redraw. We made it so that everything happens under the hood, and all the animations are exactly the same from the API point of view. A complex animation may require extracting an element from the scene into a separate CALayer and using native tools to transform it. Meanwhile, the updated scene continues to be modified behind it, with everything being fast and seamless for the user.
Testing the SVG Parser
Is there a better way to check the stability and progress of an incrementally developed SVG parser than to run the official SVG test suite? Obviously not, but it proved harder than we expected, the main reason being the rendering differences between platforms, operating systems, and devices. It meant we could not simply take a reference picture and compare it pixel by pixel with our result. We went through three different options for testing:
- Comparing models produced by the parser. We took the original image, extracted its scene model, then compared it against the model outputted by our parser. This approach seemed to work until we noticed that some images looked horribly wrong when rendered, even if the model hierarchy was the same.
- Machine learning. In an attempt to automate the process of comparing images, we briefly tried using machine learning, sophisticated image processing, and complex math for the task. While fun and entertaining for us as engineers, the results were still not great, and the time spent was too much for an open-source project. After this, we settled on the third and final approach to testing.
- Manually verifying reference images. What we ended up doing was manually comparing our rendered image with the image from the W3C test suite, then later using the verified image as reference for future tests. Despite being a slow process to verify each test manually, it gave the best practical results.
Undertaking the significant effort to create a viable testing method helped us grow the parser coverage of the SVG standard from 10% to 40%. While it doesn’t look impressive on paper, 40% covers all but the most esoteric vector graphics. It also makes Macaw one of the best SVG parsers currently available for iOS.
With Macaw, iOS users now have a convenient tool for efficiently creating rich graphical interfaces, and it has become the main library for SVG support with over five thousand stars on GitHub.
When Apple introduced Swift UI in XCode 11, it rendered a part of Macaw obsolete. Swift UI is just the declarative UI building tool we needed and could not find five years ago.
However, we don’t think it will be the end of Macaw. Our path forward is in continuous improvement and growth of our SVG parser, since in this area our tool is still one of the best tools available.