-
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-1745 Use single handler for UIKit and SwiftUI views instrumentation #657
RUMM-1745 Use single handler for UIKit and SwiftUI views instrumentation #657
Conversation
Sources/Datadog/RUM/Instrumentation/Views/RUMViewsHandler.swift
Outdated
Show resolved
Hide resolved
|
||
func rumView(for viewController: UIViewController) -> RUMView? { | ||
numberOfCalls += 1 | ||
return .init(name: .mockRandom()) |
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.
It differs from the original test implementation: If the predicate rejects the vc, it is not added to the stack.
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.
Nice 👌, this clearly shows the improvement over previous approach, where UIKit handler would be invoked 5 times for this test 👍.
@@ -9,7 +9,7 @@ import XCTest | |||
|
|||
private extension ExampleApplication { | |||
func tapButton(titled buttonTitle: String) { | |||
buttons[buttonTitle].tap() | |||
buttons[buttonTitle].safeTap(within: 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.
Mitigating flakiness
cf6cc10
to
fa81f82
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 like a very good approach 👌. I left few notes, but to me this is a good direction 👍.
Sources/Datadog/RUM/Instrumentation/Views/SwiftUI/SwiftUIViewHandler.swift
Outdated
Show resolved
Hide resolved
private func findHighestPresentedViewControllerInHierarchy(of viewController: UIViewController) -> UIViewController? { | ||
if let presentedVC = viewController.presentedViewController { | ||
return findHighestPresentedViewControllerInHierarchy(of: presentedVC) | ||
} | ||
return viewController | ||
} | ||
|
||
private func findTopViewControllerInHierarchy(of viewController: UIViewController) -> UIViewController? { | ||
if let tabBarVC = viewController as? UITabBarController { | ||
guard let selectedVC = tabBarVC.selectedViewController else { | ||
return tabBarVC // when `UITabBarController` displays no view controllers | ||
} | ||
// When `UITabBarController` has selected VC, continue searching, as the selected VC | ||
// may be containing another hierarchy (e.g. `UINavigationController`). | ||
return findTopViewControllerInHierarchy(of: selectedVC) | ||
} else if let navigationVC = viewController as? UINavigationController { | ||
guard let topVC = navigationVC.topViewController else { | ||
return navigationVC // when `UINavigationController` displays no view controllers | ||
} | ||
// When `UINavigationController` has top VC, continue searching, as the top VC | ||
// may be containing another hierarchy (e.g. `UITabBarController`). | ||
return findTopViewControllerInHierarchy(of: topVC) | ||
} else { | ||
return viewController | ||
} | ||
} |
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.
All these are smoothly supported with the new approach of tracking the stack
? Or do you think there are still some edge cases we need to look closer at?
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.
UITabBarController
and UINavigationController
are both supported by the stack, it send events in the same manner. I did some manual monkey testing to validate it, and integration tests are both green. @ncreated do you see any other checks I should run? (don't know if e2e tests applies here).
I can think of one edge case which I need to verify: When presenting multiple modal page sheet and one of them is rejected by the predicate. Then the back navigation can be inconsistent:
- start view 1
- reject view 2
- start view 3
- Dismiss view 3 -> will restart view 1 ❌
This is due to the fact that the vc below a page sheet does not disappear, in this case view 1 will still be in the stack
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 see 👍. Yes, this new approach will require quite manual testing (not only passing integration tests), as we're entirely changing the model. The initial reason for introducing UIKitHierarchyInspector
was to deal with the complexity of transitions between UIViewControllers
, notably modal view controllers. Test fixtures were added to Example
in #271 and the UIKitHierarchyInspector
was done in #278.
Our integration tests do not cover all complexity (that would be tedious), and there might be still scenarios worth manual testing, e.g.:
- changing the entire navigation tree, by
window.rootViewController = ...
; - dismissing first VC in the modal hierarchy of
N
VCs (it should dismiss the entire hierarchy); - changing
navigationController.viewControllers
stack programmatically; - jumping by few steps at once in the VCs hierarchy (e.g. imagine app receiving deeplink and handling it by pushing one VC to the stack + opening modal with another navigation VC).
I also remember that our UIKitHierarchyInspector
was not well compatible with UISearchController
and handling it required additional logic in the "views predicate". This compatibility could be best tested when dogfooding a branch in our Datadog iOS App, as it uses UISearchController
in many places. We should have a look into this at the end, before final merge (I'll be happy to help with this and manual testing as well 🙌).
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 have performed and validate the following manual testing by modifying the Example scenarios:
- changing the entire navigation tree, by
window.rootViewController = ...
✅ - dismissing first VC in the modal hierarchy of
N
VCs, it should dismiss the entire hierarchy ✅ - changing
navigationController.viewControllers
stack programmatically ✅ - jumping by few steps at once in the VCs hierarchy (e.g. imagine app receiving deeplink and handling it by pushing one VC to the stack + opening modal with another navigation VC). ✅
I still need to test it with a UISearchController
!
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 still need to test it with a UISearchController!
Keep in mind we don't yet support UISearchController
very well - it still requires manual tweaking in the implementation of predicate
to support search bar transitions to embedded VCs. I think it will be enough to just do regression test on UISearchController
and check if it gets recognised by UIKit
instrumentation as a whole. Solving the problem with search bar transitions to embedded VCs is beyond the scope of this PR.
Sources/Datadog/RUM/Instrumentation/Views/RUMViewsHandler.swift
Outdated
Show resolved
Hide resolved
Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewIdentityTests.swift
Show resolved
Hide resolved
private func findHighestPresentedViewControllerInHierarchy(of viewController: UIViewController) -> UIViewController? { | ||
if let presentedVC = viewController.presentedViewController { | ||
return findHighestPresentedViewControllerInHierarchy(of: presentedVC) | ||
} | ||
return viewController | ||
} | ||
|
||
private func findTopViewControllerInHierarchy(of viewController: UIViewController) -> UIViewController? { | ||
if let tabBarVC = viewController as? UITabBarController { | ||
guard let selectedVC = tabBarVC.selectedViewController else { | ||
return tabBarVC // when `UITabBarController` displays no view controllers | ||
} | ||
// When `UITabBarController` has selected VC, continue searching, as the selected VC | ||
// may be containing another hierarchy (e.g. `UINavigationController`). | ||
return findTopViewControllerInHierarchy(of: selectedVC) | ||
} else if let navigationVC = viewController as? UINavigationController { | ||
guard let topVC = navigationVC.topViewController else { | ||
return navigationVC // when `UINavigationController` displays no view controllers | ||
} | ||
// When `UINavigationController` has top VC, continue searching, as the top VC | ||
// may be containing another hierarchy (e.g. `UITabBarController`). | ||
return findTopViewControllerInHierarchy(of: topVC) | ||
} else { | ||
return viewController | ||
} | ||
} |
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 still need to test it with a UISearchController!
Keep in mind we don't yet support UISearchController
very well - it still requires manual tweaking in the implementation of predicate
to support search bar transitions to embedded VCs. I think it will be enough to just do regression test on UISearchController
and check if it gets recognised by UIKit
instrumentation as a whole. Solving the problem with search bar transitions to embedded VCs is beyond the scope of this PR.
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.
pr lgtm but i was wondering if we could steer our users away from using RUMViewModifier
with small views?
for example there can be many fragments in the screen at once in Android, i guess now it's potentially the same in iOS with SwiftUI. is it somehow possible to disallow using RUMViewModifier
with list cells or small views like that?
Sources/Datadog/RUM/Instrumentation/Views/RUMViewsHandler.swift
Outdated
Show resolved
Hide resolved
@buranmert That is true, but we have no way of knowing the rendered view ratio or presentation context sadly. We could use something like |
db8df52
to
c807806
Compare
2d3e62d
to
a1d113d
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.
Great work 🏅, it looks good to me 👍.
|
||
func rumView(for viewController: UIViewController) -> RUMView? { | ||
numberOfCalls += 1 | ||
return .init(name: .mockRandom()) |
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.
Nice 👌, this clearly shows the improvement over previous approach, where UIKit handler would be invoked 5 times for this test 👍.
a1d113d
to
3dd94c2
Compare
What and why?
UIHostingController
andUIViewControllerRepresentable
provide bridges betweenSwiftUI
andUIKit
allowing navigation between the two UI frameworks.Therefore, we need to perform a consistent view tracking when instrumenting both
SwiftUI.View
andUIKit.UIViewController
.How?
Single View Handler
#645 has introduced a new handler for instrumenting
SwiftUI.View
only:SwiftUIRUMViewsHandler
.Both
UIKitRUMViewsHandler
andSwiftUIRUMViewsHandler
work similarly for forward navigation but adopt different solutions for tracking backward navigation.Backward navigation tracking of
UIKit.UIViewController
uses a view-controller hierarchy inspector to retrieve the currently visible view, this approach is incompatible withSwiftUI
since we cannot retrieve the declaredSwiftUI.View
from the view-controllers hierarchy.The
SwiftUIRUMViewsHandler
, on the other hand, uses a stack of views to keep track of the appeared views and re-start them when needed during backward navigation. This approach is ui-framework agnostic and can easily be applied toUIKit.UIViewController
.The goal of the PR is to showcase the usage of a stack to track both
SwiftUI.View
andUIKit.UIViewController
in a single handler:RUMViewsHandler
Tests
The same unit tests applied to
UIKitRUMViewsHandler
now run onRUMViewsHandler
.The integration tests
RUMSwiftUIScenarioTests
now perform hybrid navigation betweenSwiftUI
andUIKit
. Any other integration test stays untouched.Review checklist