SwiftUI limitations for Mobile Analytics

In this blog post, I will introduce you to a few Mobile Analytics solutions, explain how they can automatically capture UIKit-based events and screens, and tell you about the limitations of SwiftUI.

Mobile analytics helps you to capture data from your mobile app to identify unique users, track their journeys, record their behavior, and report on your app’s performance.

To capture data you might be required to instrument your app manually, i.e explicitly call a function on a Mobile analytics library, such as logEvent(name:parameters:).

Analytics solutions like Google Firebase, Segment or Mixpanel or Dynatrace may be able to identify and automatically capture

  • user interactions and/or
  • presented screens/views.

Mobile analytics providers use a technique called method sizzling.

Method swizzling is the process of changing the implementation of an existing selector. It’s a technique made possible by the fact that method invocations in Objective-C can be changed at runtime, by changing how selectors are mapped to underlying functions in a class’s dispatch table.

Imagine you want to track how often each view controller is presented to a user in your iOS app. You could add tracking code to each ViewControllers own implementation ofviewDidAppear:. But method sizzling is a better option, especially to avoid code duplication.

extension UIViewController {
    @objc dynamic func _swizzled_viewDidAppear(_ animated: Bool) {
        _swizzled_viewDidAppear(animated)

        // do your own stuff, e.g. logging
        print("UIViewController \(self.description) will appear")
    }

    static func swizzleViewDidAppear() {
        let selector1 = #selector(UIViewController.viewDidAppear(_:))
        let selector2 = #selector(UIViewController._swizzled_viewDidAppear(_:))
        let originalMethod = class_getInstanceMethod(UIViewController.self, selector1)!
        let swizzleMethod = class_getInstanceMethod(UIViewController.self, selector2)!
        method_exchangeImplementations(originalMethod, swizzleMethod)
    }
}

Matt wrote the excellent article Method Swizzling on that topic. Now you know how Mobile Analytics solutions use this technique for auto-instrumentation. The required initialization call (here: UIViewController.swizzleViewDidAppear()) is normally hidden in the initialization call of the Mobile analytics library.

This works well for UIKit development but not for SwiftUI. So why is that?

In essence, certain UI controls in SwiftUI, e.g. Text, Image and Button , are not based on UIKit or AppKit UI Components and necessary metadata information cannot be extracted.

This post in Apple's Developer Forum illustrates it pretty well: it is possible to capture events but without the UI element identity. It is impossible to read the value of accessibilityLabel property which was set earlier.

// MARK: Swizzling Code
extension UIApplication {
  @objc dynamic func newSendEvent(_ event: UIEvent) {
    newSendEvent(event)

    if (event.allTouches != nil){
      let touches: Set<UITouch> = event.allTouches!
      let touch: UITouch = touches.first!

      OperationQueue.main.addOperation(){
        if let tView = touch.view {
          print("------------------------------------------")
          print(Mirror(reflecting: tView).subjectType)
          print("accessibilityLabel : \(tView.accessibilityLabel ?? "null")")
        }
      }
    }
  }
}
// MARK: - Demo App Code
struct ContentView: View {
  @State private var segmentValue = 0
  @State private var textValue = ""
  @State private var sliderValue: Float = 0

  var body: some View {
    Form {
      Section(header: Text("UI CONTROLS").font(.title).padding(.vertical, 0.0)) {

        TextField("TextField", text: $textValue)
          .accessibilityLabel("Text1")

        VStack(spacing: 5) {
          Text("Segment Control (\(segmentValue))")
          Picker(selection: $segmentValue, label: Text("Segment Control").padding(10.0)) {
            Text("Red").tag(0)
            Text("Green").tag(1)
            Text("Blue").tag(2)
          }
          .pickerStyle(SegmentedPickerStyle())
          .padding(10.0)
          .accessibilityLabel(Text("SegmentControl1"))
        }

        Slider(value: $sliderValue, in: 0...100, step: 1)
          .padding(10.0)
          .accessibilityLabel("Slider1")

        Button(action: {
          print("Button1 pressed")
        }, label: {
          Text("Button 1")
        })
        .accessibilityLabel("Button1")
      }
    }
    .onAppear(){
      let uiAppClass = UIApplication.self
      let currentSendEvent = class_getInstanceMethod(uiAppClass, #selector(uiAppClass.sendEvent))
      let newSendEvent = class_getInstanceMethod(uiAppClass, #selector(uiAppClass.newSendEvent))
      method_exchangeImplementations(currentSendEvent!, newSendEvent!)
      print("sendEvent Swizzled")
    }
  }
}

The result of print("accessibilityLabel : \(tView.accessibilityLabel ?? "null")") is always null.

Due to this limitation, I cannot find a Mobile Analytics provider that offers automatic instrumentation for a SwiftUI app.

  • Firebase
    • On Apple platforms, Firebase depends on method swizzling to automatically log screen views. SwiftUI apps must manually set screen names for views that should be logged via the FirebaseAnalyticsSwift module, or log screen views

  • Segment: manual instrumentation example
  • Mixpanel: manual instrumentation example
  • Dynatrace: limitation description
    • I can confirm that pure SwiftUI is not yet supported, auto-instrumentation will not work for most UI elements and needs to be performed manually. Webrequest instrumentation will still work, but automatic linking to lifecycle action will not work for pure SwiftUI apps. In some scenarios automatic linking to manual actions might work. The SDK for manual instrumentation will work.

Did you find this article valuable?

Support Swifty Tech by Marco Eidinger by becoming a sponsor. Any amount is appreciated!