dictionary Image by Andrea Piacquadio from Pexels

If you can’t measure it, you can’t improve it – unknown

We cannot have a great product without some network component in it. When we talk about network, we’re not only referring to fetching data in a server and pushing updates into it. We’re referring to word of mouth, sharing some cool feature/milestone with your close ones and even posting it on social media.

How do we embed this behavior within our iOS apps? Let’s see what we have for the menu this time:

First part of the equation

UIKit provide us with the UIActivityViewController class to achieve precisely this.

import UIKit

final class SocialMediaViewController: UIActivityViewController {
    init(qrImage: UIImage, catalogName: String) {
        let message = "These are my missing stickers for \(catalogName)"
        super.init(activityItems: [message, qrImage], applicationActivities: nil)
        excludedActivityTypes = [.assignToContact,
                                 .addToReadingList,
                                 .markupAsPDF,
                                 .openInIBooks,
                                 .postToFlickr,
                                 .print,
                                 .saveToCameraRoll]
    }
}

The above snippet is extracted from the sharing QR funnel in MyStickers. Now it is as simple as instantiating a SocialMediaViewController and presenting it.

Enter tracking to the picture

As solo developers in a bootstrap project, we often need to make choices related to focus. The beautiful thing about coding (for those of us who love it) is that it’s an endless endeavor. However, at the end of the day all that code matters only if it’s actually being used. How do we know our sharing popup is actually being shown?

We need to track it down. Let’s suppose we’re using the TrackingEngine package to achieve this. We could log an event in our Coordinator whenever the SocialMediaViewController is presented like so 👇🏽

func launchSharing(with qrImage: UIImage) {
    let socialSharing = SocialMediaViewController(qrImage: qrImage, catalogName: "Qatar 2022")

    rootViewController.present(viewControllerToPresent, animated: true) { [weak self] in
        self?.tracker.track(
            eventName: "share_initiated",
            parameters: ["name": storageController.currentCatalogueName]
        )
    }
}

Now we can track whenever our users use started the sharing feature…

Is it actually working?

Garbage in, garbage out

But wait, this only tracks when the users initiated the sharing funnel. It doesn’t actually track when they fulfill it. Our statistics and measures are only as sound as the input we feed them with. Let’s be more precise.

The UIActivityViewController has an optional typealias called completionWithItemsHandler which is a closure that gets executed whenever the activity itself was fulfilled (or dismissed). Said closure expects 4 parameters:

  • activityType (UIActivity.ActivityType?): it refers to the type of service selected by the user. This could be useful to track down which platforms are converting more and put more efforts into customizing said flows.
  • completed (Bool): The only moment when this is true is when the user actually completed the sharing itself.
  • returnedItems (Any?): For custom activities that involve modifying data by its extensions, this value is useful. It returns any changes made to the original data.
  • activityError (Error?): if something went wrong, this value will tell us what was it. In case the activity completed smoothly, it’ll be nil

With all this context we can now track more confidently our flow

func launchSharing(with qrImage: UIImage) {
    let socialSharing = SocialMediaViewController(qrImage: qrImage, catalogName: "Qatar 2022")

    socialSharing.completionWithItemsHandler = { [weak self] item, completed, values, error in
         self?.trackSharing(itemType: item, 
                            wasSharingCompleted: completed, 
                            values: values,
                            receivedErrors: error)
    }

    rootViewController.present(viewControllerToPresent, animated: true) { [weak self] in
        // Anonymous closure executed after the `present` action is done
        self?.tracker.track(
            eventName: "share_initiated",
            parameters: ["name": storageController.currentCatalogueName]
        )
    }
}

func trackSharing(itemType: UIActivity.ActivityType?,
                  wasSharingCompleted: Bool,
                  values: [Any]?,
                  receivedErrors: Error?) {
        guard wasSharingCompleted else {
            // Sharing action got cancelled by the user 😕
            tracker.track(eventName: "share_canceled", parameters: nil)
            return
        }

        var parameters: [String: Any] = ["method": itemType?.rawValue ?? "N/A"]

        if let shareError = receivedErrors?.localizedDescription {
            // Something went wrong and we track down exactly what 🧐
            parameters["error"] = shareError
            tracker.track(eventName: "share_failed", parameters: parameters)
            return
        }

        // Jackpot! The user went through the entire funnel 🥳
        tracker.track(eventName: "share_executed_successfully", parameters: parameters)
    }
}

Final thoughts

There are countless reasons why our users might not go all the way through the sharing funnels. Just to mention a few of then:

  • An incoming call and later forgetting to return to the app
  • The app crashing whenever the sharing started feature gets started
  • The method they chose isn’t is fully supported by our apps yet

Only by careful tracking each steps of their journey can we hone in the data and tweak our assumptions to match their reality, thus crafting an optimal UX for them.