By Dom Chapman
Since launching in 2009, Uber has expanded its mission of making transportation more reliable to the delivery of food, kittens, ice cream, and everything in between. The Uber API makes it easy for developers to build apps that coordinate the movement of all sorts of things, but each of these technologies have varying requirements.
For example, when we launched UberRUSH in 2015, its rapid growth required us to quickly add new functionalities as businesses with different delivery requirements joined the platform; one critical security feature required by many businesses was proof-of-delivery via signature.
In order to meet this requirement, we needed a way to collect signatures to confirm delivery on UberRUSH. We decided the most efficient way to do this was by adding a signature rendering feature to the Uber Partner iOS app. By including this functionality in the app, the delivery-partner would be able to quickly and easily collect a signature from the recipient directly on their device.
Today, we open sourced UberSignature, a new feature that allows users to draw and store signatures on the UberRUSH app. This article outlines how we engineered UberSignature (displayed above) using an existing algorithm with a few modifications to tailor the solution for our delivery-partners’ needs. We also explain how we made touchscreen signatures on the Uber Partner app look more natural.
Building UberSignature for iOS Research
One of the main challenges of developing UberSignature was to ensure that the signature rendering code was performant on all types of mobile devices, some of which are more than five years old yet are still used by many delivery-partners. The feature also needed to be cleanly architected, so any Uber engineer could maintain it crash-free. This is particularly important because the feature is consumer-facing; a delivery-partner restarting the partner app during the drop-off transaction affects their efficiency and also causes issues for recipients.
iOS does not come with any built-in classes for directly drawing on a smartphone screen with touches, and we wanted a solution that would allow users to “sign” a natural, smooth-looking signature. Luckily, this is not a new problem in the world of iOS. There are many resources available online that address this issue. Through our research, we discovered a thorough March 2013 article, “iOS SDK: Advanced Freehand Drawing Techniques,”on an advanced drawing algorithm that produced results well-suited to our needs.
This solution draws two parallel bézier paths at varying distances from each other that are connected by straight lines at each end, producing a smooth-looking curve with continuously changing thickness. Other solutions change the thickness property of the line at each segment; this is problematic because the changes in thickness are abrupt, giving the signature an unnatural look.
We based our code on the parallel curves method, but extended the implementation and incorporated some new features:
- Signature updates with every touch: Instead of waiting for every four touch points to draw a full bézier curve, we wanted a solution that would update the signature at every touch. As we cannot draw a bézier curve with less than four points, we decided to draw what we could with the points we did have: a dot with one, a line with two, and a quad curve (a curve with a single control point) with three. With this approach, the signature changes shape at each touch, correcting itself after every touch point is recorded. This makes the implementation more complex, but the drawing feels a lot more responsive and the signature lines end where the touches end, making the signature look much more fluid and natural.
- New signature weight calculation: We changed how the signature weight was calculated. Rather than getting thicker as the user’s finger moves faster, we found that making the signature appear thinner would more closely mimic the look of pen on paper.
- Ability to draw dots: We added the ability to create dots when tapping the view, which was important for allowing users to accurately draw their signatures.
- Break down the implementation into multiple classes: We built a view controller that could handle being resized while still offering functionalities for resetting, retrieving the current signature, and initializing with a previous signature image. Under the hood, we broke the implementation down into five classes with fewer responsibilities. This made the code easier-to-understand, debug, and maintain.
Designing the Architecture
To enable these functionalities and facilitate more natural-looking signatures on the app, we created a signature algorithm with five distinct classes, depicted below:
In the following sections, we outline how each of these classes fit into the UberSignature architecture:
The rendered signatures exhibit different weights associated with each point. So we came up with a typedef struct to represent this, called UBWeightedPoint:
We then made a category on UIBezierPath that creates our paths from weighted points. It was built with methods that create different UIBezierPaths for up to four weighted points, as demonstrated below. This is needed for our extended correcting technique, where we update the signature at every touch.
This category neatly wraps all the logic needed to draw the signature béziers. The drawing and calculation techniques here are similar to our original inspiration, abstracted away from state and view code.
This object responds to the method call addPointToSignatureBezier: and generates the signature, providing it as a combined temporary (constantly changing) path and finalized paths. The bézier gets longer until four points are collected, enough for a full bézier curve. As displayed in Figure 3, the finalized path always returns the finalized lines, with the temporary path returning the part of the line still being drawn.
The method for adding new touch points is calculated using the standard linear equation, y = mx + c, which finds the point’s associated weight based on its distance from the previous touch point. In this equation, x is the distance between points subtracted from a maximum distance, c (the constant) decides the minimum weight of the line, m (the gradient) controls the rate at which the weight decreases as length changes, and y is the returned weight. The relationship between these variables results in a smaller weight as the distance increases, up to the maximum distance.
As points are added, the provider calls its delegate with an updated temporary bézier, formed using the UIBezierPath category methods with the newly calculated weighted points, as depicted below:
Once the touch point for the next bézier has been added, the current bézier’s last point becomes the average of the preceding point and the point that comes next, a tactic that smooths the join between them. At this time, the current bézier will not change, so the delegate is notified with this finalized path. The temporary bézier picks up again, using the last point of the finalized bézier as its first point and the new point as its second. This process repeats indefinitely until the provider is reset, starting a new line.
The UBSignatureDrawingModel is a class that uses an instance of the UBSignatureBezierProvider to generate the signature, storing it and exposing it as two objects: a UIImage and a (temporary) UIBezierPath.
Using a single bézier to represent the whole signature does not scale well; it would have to be drawn during every update, each time with more points, thus increasing the time it takes to render a signature on screen whenever it changes. We use an image to represent the signature, with the béziers from the provider drawn on it each time they are finalized. Although the image takes slightly longer to draw than a small bézier, it is a constant time operation and scales well (and quickly) when compared to other methods.
Our model also stores and exposes the temporary path of the provider. We cannot draw this into the image, because during the next update it will change shape and we cannot remove the previous path from the image. Instead, we store and display the temporary path separately. This could be displayed as another image, but it is less performant to render another full-size image than a small bézier path that will never exceed four points.
Since the signature is represented by an image, it matches the dimensions of the view. If the size of the view being backed by the model changes, the model can be asked to resize the image. Before resizing, it will draw the temporary bézier into the image, as scaling our bézier path and calculating a scaled weight is tricky and will not align well with the resized image.
The UBSignatureDrawingModelAsync is a wrapper around the model, allowing it to be used asynchronously so it does not block the app’s main thread. This is important as the model is computationally expensive; running it on the main thread will reduce the frequency with which we can capture touches, significantly affecting signature accuracy.
To avoid complicating the implementation, the underlying model is designed synchronously and does not make use of background threads. The async wrapper then uses the model on an NSOperationQueue and the main thread for synchronous methods. The model property is atomic to avoid multiple threads accessing the model simultaneously, thereby abstracting the complexity of asynchronous code into a single class. This also means we can still use the synchronous model directly. In the case of debugging, this makes stepping through our code a lot easier to follow.
The view controller, named UBSignatureDrawingViewController, encapsulates our signature feature. It uses the asynchronous model internally, overriding the UIResponder touch events to add points, then displays the signature image via a UIImageView and the temporary bézier path using a CALayer. It also handles resizing events and exposing an image method for obtaining the signature when complete.
As smartphones become increasingly important to commerce, UberSignature provides a streamlined and more typographically accurate way of drawing signatures on iOS using only touch. We hope that developers can benefit from using UberSignature for their own apps.
Dom Chapman is a senior software engineer with the Uber Everything team based in Uber’s New York City office.