For customers who are already familiar with using Fullstory on iOS with UIKit or React Native apps, Fullstory’s SwiftUI support is a bit different. In this document, you’ll learn:
- The differences between UIKit and SwiftUI in iOS
- How those differences affect how you integrate Fullstory into your application
- The subtle differences in how Fullstory computes selectors
- Some known limitations with our SwiftUI integration
This article is a companion to the Integrating Fullstory into a SwiftUI App article, and users who want to implement Fullstory in a SwiftUI application should follow those steps.
For customers who were part of the Fullstory SwiftUI early access program, please be aware the mechanism of enabling SwiftUI support has changed to an Info.plist key; customers upgrading to 1.31.0 or later will need to enable that key in order to continue to use Fullstory SwiftUI APIs.
Why is Fullstory on SwiftUI different from Fullstory on UIKit?
UIKit vs SwiftUI on iOS
As you are already likely aware, the SwiftUI model of constructing a user interface is very different from the classical UIKit model of constructing a user interface. In the UIKit model, the application programmer is responsible for instantiating individual UIView objects, which persist over a long period of time and represent a given on-screen element. To interact with these elements, Fullstory annotates each UIView’s long-lived representation in memory with information the application programmer provides–including Fullstory class names, attributes, and tag names. Fullstory then refers back to these to embed them in the captured session when it periodically “scans” the layer hierarchy.
In the SwiftUI model of user interface construction, the application programmer does not explicitly manage the lifecycle of a View; the programmer-visible representation of an on-screen element, a View, can be (and often is!) implicitly recreated at any point based on updates to the contents of the screen. SwiftUI internally manages mappings of these Views to elements that render on-screen; by the time a representation hits the display, it is very possible the programmer-visible View may no longer exist in memory at all.
How Fullstory for SwiftUI is affected
These differences pose a challenge for the Fullstory model of tagging long-lived representations, and then periodically scanning the screen for updates– the semantic information (the View) may not have a one-to-one representation of on-screen layers, and the semantic information underlying the contents of the screen may no longer even exist. As a result, in the current implementation, Fullstory can only propagate explicitly annotated class names through to SwiftUI session capture, rather than automatically inferring information about the view hierarchy the way you may expect with UIKit. This also means that Fullstory privacy rules will always require code changes, and there are some subtle differences in how the selector structure is formed by the hierarchy of SwiftUI Views.
Because we expect to improve our ability to infer SwiftUI selectors in the future, we’re introducing a new setting called SwiftUI Selector Versions. The current version has the above limitations, but future versions may support automatically generated selectors, and may improve on how the selector hierarchy is represented. These are great features for getting richer analytics about your application, and to reduce the amount of instrumentation that you need to do in your code, but selectors that change could have the side effect of breaking privacy rules or analytics.
When you set the SwiftUI Selector Version in your application, all Fullstory plugin upgrades that support that selector version will continue to generate compatible selectors until you’re ready to upgrade – and once a Fullstory plugin no longer supports that selector version, it will throw an assertion immediately on startup so that you know. Setting the SwiftUI Selector Version in your application will future-proof your implementation of Fullstory in your app.
For applications that integrate a prior version to 1.31.0, or don’t enable SwiftUI support in Info.plist, there is very limited support for Session Replay in SwiftUI Views, and all the content will be masked without any way to unmask or exclude portions of the View.
What’s different when using Fullstory on SwiftUI applications?
So what’s different about using Fullstory? You’ll need to enable SwiftUI support explicitly and specify your selectors and masking rules in code. There are also some limitations to the types of selectors you can create, and some nuances that make some Fullstory features work slightly differently than in UIKit.
Note: This article applies to SwiftUISelectorVersion 1 and 2. At the time of writing, these are the only versions available.
Overview of SwiftUI Support Differences
The below table provides an overview at a glance of the features supported in four different configurations: a UIKit app; a SwiftUI app, but with Fullstory’s SwiftUI support disabled; a SwiftUI app with Fullstory’s SwiftUI support enabled, but without committing to a selector version; and a SwiftUI app, with selector version 1 or 2. Version 2 is available with Fullstory for Mobile Apps version 1.49.0 or later.
Feature |
UIKit |
SwiftUI (support disabled) |
SwiftUI (no selector version)* |
SwiftUI (selector version 1/2) |
Private-by-Default Session Capture |
Yes |
Yes |
Yes |
Yes |
Individual view privacy settings (.fsMask, etc.) |
Yes (UIView) |
No (SwiftUI.View) |
Yes (SwiftUI.View) |
Yes (SwiftUI.View) |
Adding classes to views (.fsAddClass) |
Yes |
No |
Yes |
Yes |
Creating rules for classes manually in Fullstory web interfaces |
Yes |
No |
Yes |
Yes |
Suggested selectors in Fullstory Page Insights inspect mode |
Yes |
No |
No |
Yes |
Tap events / rage taps |
Yes |
No |
No |
Yes |
Automatic (codeless) generation of selectors |
Yes |
No |
No |
No |
*We included what happens if you enable SwiftUI Support and don’t set SwiftUI Selector Version, but there’s no benefit to not specifying it.
Differences in Selectors for SwiftUI
With UIKit, the Fullstory API allows you to specify all the parts of the selector, including using accessibilityIdentifier to specify the #id
of the element, FS.setAttribute()
to specify any [attr="some"]
CSS style attributes, and FS.addClass()
to specify the element’s CSS .class
. Additionally, Fullstory automatically uses the UIView’s Objective-C class as the element’s Tag, which means you can create a full CSS selector like the following example:
UIButton#checkoutButton.primaryAction > UILabel[variant="checkout-variant-A"]
For SwiftUISelectorVersion 1 or 2, Fullstory does not do any automatic detection of a View’s properties, and the only selector parts you can specify are .class
via the .fsAddClass()/ .fsAddClasses()
modifiers, or [attributeKey=attributeValue]
via the .fsSetAttribute()/ .fsSetAttributes()
modifiers. Additionally, with SwiftUISelectorVersion 1, because of the way Stack layouts like HStack
and VStack
work in SwiftUI, classes you specify on a View may propagate to its subviews. For example, if you had a layout like so:
VStack(alignment: .center) {
Text("Hello World")
.fsAddClass("hello_world")
}
.fsAddClass("hello_world_container")
… the VStack
’s selector would be .hello_world_container
, and the Text
’s selector could be .hello_world_container.hello_world
(This behavior depends on the specific Views that are enclosed, and their parent container; it is also possible that the Text
’s selector could be .hello_world
, leading to a true parent-child structure like .hello_world_container > .hello_world
)
If you’re familiar with how CSS selectors work, or how selectors work in Fullstory for UIKit or Android, you’ll notice this is a deviation from the normal semantics of how those selectors are computed. In SwiftUISelectorVersion 2, classes and properties that you add to a View will more strictly follow the hierarchy of Views that you apply them to. This means that when upgrading from SwiftUISelectorVersion 1 to 2, the selector may change from .hello_world_container.hello_world
to .hello_world_container > .hello_world
.
SwiftUI Nuances
Most of the Fullstory features you know and love like Rage Taps, Dead Taps, Network Capture, Metrics, Dashboards, Conversions, Funnels, and Session Replay all work the same with our SwiftUI integration as they do with our Android, React Native, and UIKit integrations. With that, there are a few nuances that are helpful to know when using Fullstory to dive into your sessions.
Switch to SwiftUISelectorVersion 2 to disambiguate taps on a View and taps on a subview of that View.
In the above example, with SwiftUISelectorVersion 1, both the VStack
and the Text
element share the same root class name, and they don’t follow a strict parent-child hierarchy. As such, it’s not possible to disambiguate between taps on the VStack
and taps on the Text
element when using Fullstory’s web UI to search for users using the Event Filter for “Clicked on CSS Selector”. If you need to disambiguate, switch to SwiftUISelectorVersion 2, or you can use a Custom Event in the handler for tap events on the interested View.
With SwiftUISelectorVersion 1, it may not always be possible to unmask a subview of a masked View if it is masked in code.
If a View is marked as masked using the .fsMask()
or .fsAddClass(.masked)
modifiers, using .fsUnmask()
on a subview of that View may not actually unmask that subview. This is for a similar reason as the above: .fsMask()
and.fsUnmask()
each add a class to a View’s selector, and if both classes apply to the same View, then the .fs-mask
class takes precedence. However, because Mobile is Private-by-Default, Fullstory defaults to masked privacy state without you needing to specify it in code. As a result, this situation should be uncommon, because this would only occur in the following situation
- You have unmasked a View high in the hierarchy using
.fsUnmask()
- You have masked a sub-view of that View using
.fsMask()
- You then have unmasked a subview of the View in item 2) using
.fsUnmask()
To avoid getting into this situation, unmask and mask Views as finely as possible when you know that you may have complex masking hierarchies, or switch to SwiftUISelectorVersion 2 to alleviate this issue.
Clicked Event filters don’t currently support searching by Click Text.
With SwiftUISelectorVersion 1 or 2, you’ll need to search by programmatic classes and/or attributes when specifying search criteria. You won’t be able to search for users who clicked on specific Text in your application.
Without SwiftUISelectorVersion
specified, Click Events won’t be searchable with the classes you add.
If you don’t add the SwiftUISelectorVersion to your Info.plist, click events won’t get processed in a way that you can properly search for them. If you’re having trouble searching for Click Events that you know are annotated with a class, check that you have properly set SwiftUISelectorVersion to 1 or 2.
Navigation Events may contain unexpected names.
Because SwiftUI names cannot be detected at runtime, SwiftUI Hosting Controllers will have opaque names in Navigation Events. Future versions of the plugin will provide clearer names for SwiftUI Navigation Events.
Switch to SwiftUISelectorVersion 2 for additional robustness improvements.
SwiftUISelectorVersion 1 does not support adding Fullstory classes or attributes to .drawingGroup(opaque:colorMode:)
. In addition, you may see WARNING: Internal SwiftUI error
console log messages with SwiftUISelectorVersion 1. These issues are resolved with SwiftUISelectorVersion 2.