iOS: How SwiftUI and generics helped us build a reliable document upload component

Tom Holmes
Thirdfort

--

Our app needed a reliable, reusable component for allowing the user to upload multiple documents to our backend. The problem we had to tackle was to build a component that was configurable to allow for various types of files and photos to be uploaded without losing type-safety. We also wanted to take advantage of SwiftUI’s expressiveness so that we could build a document list view that reacted to the model.

The component allows for multiple photos to be uploaded that can represent pages in a single document or the user could choose to upload a PDF from the iOS Document Picker. The component has a live list that the user can use to preview, edit or remove documents before submitting to the backend.

How we thought about the architecture

We knew from the start we needed a view that would be generic across journeys (proof of address, bank statements, etc.) but it needed to know about what specific documents each journey allows — for instance, the app only accepts a council tax bill, utility bill or bank statement as proof of address. We also need to consider that if the app needed new flows which have a document upload requirement in the future then this component could be used there with minimal new code, because we can just reuse it by making it aware of the current context for that journey and it will handle the rest.

This presented the problem of how do we make the component aware of the “current context” in the code. We decided on a protocol we named DocumentsScope that contains a static variable for an array of DocumentType:

protocol DocumentsScope {    static var documentTypes: [DocumentType] { get }}extension DocumentType {    static let bankStatement}

An array of DocumentType is stored as a static variable containing a various presentational values depending on the type of document the user selects to power the UI, such as utility bill, or bank statement. The DocumentType also has a property for allowedMimeTypes that determines what file types are selectable for that document such as PDFs or images.

protocol DocumentListModel {    associatedtype Scope: DocumentsScope    associatedtype Content: View    static var content: Content { get }}

This is great because we can genericise the SwiftUI View to the Scope. On top of this, we can easily add new Views in the future as all we would need to do is add a new DocumentListModel and Scope for that new journey.

The app’s document upload journeys support multiple document uploads (you have to upload two in the case of proof of ownership) so we needed a way for the GenericDocumentListView to work across those journeys for the relevant types of documents (e.g. bank statements or utility bills, etc) before the user decides they want to confirm and upload.

To enable this functionality we started with another protocol named Storage that powers and maintains the list of document URLs the user has selected as well as any business logic for appending, replacing or removing.

final class DocumentController<S: Storage>: ObservableObject {    @Published var isValid: Bool    public func upload() async throws -> Result<Void, Error>}

We made an ObservableObject that was generic on the Storage called DocumentController that publishes any changes to the storage and an isValid bool for if the required number of documents has been selected in order for the user to continue, essentially powering the GenericDocumentListView which is the object that configures the Views.

The DocumentController pulls everything together by handling all of the business logic such as the processing and the upload task and handling any errors as well as a method for updating the isValid property and locally stores the Storage object to the UserDefaults so that the user does not have to submit them then and can come back to submit them later.

Key advantages of this approach

A new requirement (at the time of writing) that we now want to add for certain document types is a UI for date validation where we ask the user to provide a date of the document to confirm its a certain age (and therefore valid to the lawyer). Because our document upload component is reliant on the Scope (for document types) we can easily add this requirement to those specific types of documents rather than having to duplicate code and deal with special cases which would be far more prone to errors and be harder to maintain.

Our approach to how we built this component has allowed the flexibility to easily add new functionality while keeping consistency across all our document upload tasks. This also means that we only have to maintain one feature rather than multiple across all our document upload tasks.

Comparing the new approach to the previous one we did have to think a lot more about navigation unlike before where it was a single view we now present a modal within the journey to select a DocumentType treating it like a “sub-journey” and then another modal for either the iOS document/photo picker or the camera. We believe this implementation is what feels most natural for a good UX as it always dismisses back to the live list once a document is selected.

This approach also will enable us to add UI features such as tracking upload progress, cancelling and/or retrying to upload that will undoubtedly improve the UX. Unlike the old approach which was to upload documents as and when the user selected each one disabling them from going back to remove or change them.

Join Thirdfort

At Thirdfort, we’re on a mission to make moving house easier for everyone involved. We use tech, design, and data to make working with clients secure and friction-free for lawyers, property, and finance professionals. Since launching in 2017, we’ve grown from a single London office to an international team with sites in Manchester and Sri Lanka. We’re backed by leading investors like Alex Chesterman (Founder of Zoopla and Cazoo) and HM Land Registry.

Want to help shape Thirdfort’s story in 2022? We’d love to hear from you. Find your next role on our Careers Hub or reach out to careers@thirdfort.com.

--

--