-
Notifications
You must be signed in to change notification settings - Fork 134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RUMM-1615 SwiftUI View Instrumentation #645
Conversation
4a5066b
to
73adca2
Compare
buttons["Push Next Screen"].safeTap(within: 5) | ||
} | ||
|
||
func tapBackButton() { | ||
navigationBars["Screen 4"].buttons["Screen 3"].tap() | ||
navigationBars["Screen 4"].buttons["Screen 3"].safeTap() | ||
} | ||
|
||
func tapPopToTheFirstScreenButton() { | ||
buttons["Pop To The First Screen"].tap() | ||
buttons["Pop To The First Screen"].safeTap() | ||
} | ||
|
||
func swipeInteractiveBackGesture() { | ||
let coordinate1 = coordinate(withNormalizedOffset: .init(dx: 0, dy: 0.5)) | ||
let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.75, dy: 0.5)) | ||
let coordinate2 = coordinate(withNormalizedOffset: .init(dx: 0.80, dy: 0.5)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not related to SwiftUI
but I've noticed some flakiness in this test which I'm mitigating here.
73adca2
to
40a1150
Compare
45441bd
to
dc87558
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very good 🚀. I left few comments and questions.
Sources/Datadog/RUM/AutoInstrumentation/Views/SwiftUIViewModifier.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great in the code 👌. However, when I played with this scenario on Simulator, I found one bug 🐞 when canceling interactive back gesture from UIKit
view controller. Check the GIF below and see how our RUM debug outline view (at the bottom) disappears - meaning that there is no active view at that moment.
Seems that SwiftUI
starts the view, then cancels it, but then UIKit
doesn't own the previous view. AFAIR, in UIKit
this case is handled from UIKitRUMViewsHandler
and its notify_viewDidDisappear(viewController:animated:)
invoked by the cancelled VC (the "back" view in navigation sense). This might be a tricky case to solve 💡 (this sort of complexity was one reason to introduce UIKitRUMViewsHandler.swift
) but it's definitely something we must handle for apps mixing UIKit
+ SwiftUI
. You can debug this concept in UIKit
by running this scenario:
Sources/Datadog/RUM/AutoInstrumentation/Views/SwiftUIViewModifier.swift
Outdated
Show resolved
Hide resolved
Datadog/Example/Scenarios/RUM/SwiftUIInstrumentation/SwiftUIRootViewController.swift
Outdated
Show resolved
Hide resolved
Tests/DatadogIntegrationTests/Scenarios/RUM/RUMSwiftUIScenarioTests.swift
Outdated
Show resolved
Hide resolved
That's a tricky case indeed. The SwiftUI Hosting VCs are filtered out before reaching the |
a11dced
to
8c28680
Compare
I've made significant changes in the way we track navigation in
|
337e379
to
8f3d4d7
Compare
8f3d4d7
to
566a95b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// This stack allows to track appearing and disappearing views to consistently | ||
/// publish start and stop commands to the subscriber. The last item of the | ||
/// stack is the visible one, any items below it have appeared before but not yet | ||
/// disappeared. Therefore, they are considered not visible but can be revealed | ||
/// if the last item disappears. | ||
private var stack: [View] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this critical to deal with the complexity in SwiftUI's onAppear
/ onDisappear
callbacks or is it more a helper we introduce for "start" / "stop" commands consistency?
I'm only concerned that this might introduce a second source of truth for views tracking. RUM scopes already solve some complexity, e.g. "if view B
is started, but A
was not stopped, A
will be stopped automatically" or "if view X
is started twice, the second command will be ignored".
With this complexity solved in RUM scopes, the views handler for UIKit (UIKitRUMViewsHandler
) does not track VCs stack nor make any additional assumptions over historical state in its navigation. By tracking stack
for SwiftUI, we'd need to ensure that both handlers are in sync (i.e. modify SwiftUI stack from UIKit handler for hybrid UIKit + Swift UI apps). Won't it get very complex?
On the other hand, handling all UIKit + SwiftUI navigation specifics in RUM scopes might get complex and tricky as well. RUM scopes aim at being UI-framework agnostic and that might be tough to have uniform behaviour for SwiftUI and UIKit. What's your take on this @maxep ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is critical to deal with the mentioned use cases:
- Dismiss Presentation Sheet (modal)
- Cancel back interaction in navigation
Without it, we don't restart the visible view when necessary. With UIKit
auto-instrumentation, this is done with the help of UIKitHierarchyInspector
and a simple state management. From my understanding, the RUM View scope only performs sanity checks while UIKitRUMViewsHandler
and SwiftUIRUMViewsHandler
track the views' states.
You are right about navigating between SwiftUI
and UIKit
, it will be very complex to keep them in sync, especially with the assumptions made by UIKitHierarchyInspector
which does not work well with VC containers used in SwiftUI
. I would be more keen on having a unified/ui-agnostic handler for both in RUMInstrumentation
, I will be dealing with this issue in the next iteration (RUMM-1745) but I would be happy to chat more about possible solution!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With UIKit auto-instrumentation, this is done with the help of
UIKitHierarchyInspector
and a simple state management.
Indeed, it's all true and very good observation 👌. Thanks for more insights - clearly, we can't handle value-based SwiftUI.Views
in the same way as we do with object-based UIKit.UIViewControllers
. The stack
seems like a good, minimal approach 👍.
Speaking of RUMM-1745
, I don't have any clear idea ATM. Like you say, having a single handler that will receive both notify_viewDidAppear()
and onAppear()
callbacks could be a solution. This way we could be considering both UIKit
and SwiftUI
navigation in one place, possibly leading to more control. Another approach could be to introduce next handler that combines UIKit
and SwiftUI
handlers into single stream - in theory that would mean less refactoring, but in practise it can be not doable if the hybrid support is mainly about details.
func trackRUMView( | ||
name: String, | ||
attributes: [AttributeKey: AttributeValue] = [:] | ||
) -> some View { | ||
let path = "\(name)/\(typeDescription.hashValue)" | ||
return modifier(RUMViewModifier(name: name, path: path, attributes: attributes)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're missing coverage for this and RUMViewModifier
in unit tests. Ideally, we may instantiate the view, force-call onAppear()
/ onDisappear()
and record its interaction with stubbed SwiftUIViewHandler
. It was easy in UIKit
tests, but with SwiftUI
this might get trickier. Any idea on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot call .onAppear()/ .onDisappear()
from a SwiftUI.View
, a view only declare its body. That's why I left it out of unit tests and cover this in integration tests. I could try to workaround that by embedding the view in a UIHostController
and see if I can trigger the events from it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's not too complex, IMO we can give it a shot. Otherwise, integration tests coverage is fine 👌 and definitely better than tricky or flaky units.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a unit test using UIHostingController
. It also involve a mocked SwiftUIViewHandler
and a new RUMInstrumentation.init
.
onAppear
and onDisappear
have to be tested in a single unit because it has to wait for appearance before disappearing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I will revert the test.. It fails on iOS 14.4, I prefer reverting and sticking with the integration test to avoid introducing any flakiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
What and why?
Add
SwiftUI.View
instrumentation by providing aSwiftUI.View
extension to monitor a view with RUM.How?
UIKit Default Predicate
DefaultUIKitRUMViewsPredicate
now exclude anyUIViewController
defined inSwiftUI
framework. It will exclude the publicUIHostingController
but also any private container thatSwiftUI
creates under the hood.SwiftUI View Modifier
A
SwiftUI.View
extension provides a simple interface to start monitoring the view. The extension applies aViewModifier
that responds to the.onAppear
and.onDisappear
to respectively start and stop monitoring the view with the internalRUMInstrumentation
instance.A
ViewModifier
returns a different version of the view which allow applying other modifiers or other handlers for.onAppear
or.onDisappear
without interfering with the RUM instrumentation.SwiftUI RUM View Handler
The introduced
SwiftUIRUMViewsHandler
will keep track of the appeared views published by theRUMViewModifier
to send Start and Stop RUM view commands in a consistent way. This handler uses a stack of appeared views:This behaviour allow to re-start views when cancelling the back interaction in a navigation stack, and when dismissing a page sheet in modal navigation.
Review checklist