Skip to content
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

Merged
merged 9 commits into from
Nov 24, 2021

Conversation

maxep
Copy link
Member

@maxep maxep commented Nov 5, 2021

What and why?

UIHostingController and UIViewControllerRepresentable provide bridges between SwiftUI and UIKit allowing navigation between the two UI frameworks.

Therefore, we need to perform a consistent view tracking when instrumenting both SwiftUI.View and UIKit.UIViewController.

How?

Single View Handler

#645 has introduced a new handler for instrumenting SwiftUI.View only: SwiftUIRUMViewsHandler.
Both UIKitRUMViewsHandler and SwiftUIRUMViewsHandler 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 with SwiftUI since we cannot retrieve the declared SwiftUI.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 to UIKit.UIViewController.

The goal of the PR is to showcase the usage of a stack to track both SwiftUI.View and UIKit.UIViewController in a single handler: RUMViewsHandler

Tests

The same unit tests applied to UIKitRUMViewsHandler now run on RUMViewsHandler.
The integration tests RUMSwiftUIScenarioTests now perform hybrid navigation between SwiftUI and UIKit. Any other integration test stays untouched.

Review checklist

  • Feature or bugfix MUST have appropriate tests (unit, integration)
  • Make sure each commit and the PR mention the Issue number or JIRA reference


func rumView(for viewController: UIViewController) -> RUMView? {
numberOfCalls += 1
return .init(name: .mockRandom())
Copy link
Member Author

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.

Copy link
Member

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mitigating flakiness

@maxep maxep force-pushed the maxep/RUMM-1745/swiftui-uikit-navigation branch from cf6cc10 to fa81f82 Compare November 5, 2021 15:04
@maxep maxep requested review from ncreated and buranmert November 5, 2021 16:11
Copy link
Member

@ncreated ncreated left a 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 👍.

Comment on lines -40 to -65
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
}
}
Copy link
Member

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?

Copy link
Member Author

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:

  1. start view 1
  2. reject view 2
  3. start view 3
  4. 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

Copy link
Member

@ncreated ncreated Nov 9, 2021

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 🙌).

Copy link
Member Author

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!

Copy link
Member

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.

@maxep maxep self-assigned this Nov 10, 2021
@maxep maxep marked this pull request as ready for review November 15, 2021 10:05
@maxep maxep requested a review from a team as a code owner November 15, 2021 10:05
@maxep maxep requested a review from ncreated November 15, 2021 10:05
Comment on lines -40 to -65
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
}
}
Copy link
Member

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.

Copy link
Contributor

@buranmert buranmert left a 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?

@maxep
Copy link
Member Author

maxep commented Nov 16, 2021

@buranmert That is true, but we have no way of knowing the rendered view ratio or presentation context sadly. We could use something like GeometryReader, but this would add much more complexity in terms of integration for our users.

@maxep maxep force-pushed the maxep/RUMM-1745/swiftui-uikit-navigation branch 2 times, most recently from 2d3e62d to a1d113d Compare November 17, 2021 11:46
Copy link
Member

@ncreated ncreated left a 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())
Copy link
Member

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 👍.

@maxep maxep force-pushed the maxep/RUMM-1745/swiftui-uikit-navigation branch from a1d113d to 3dd94c2 Compare November 22, 2021 12:31
@maxep maxep merged commit b01284d into feature/swiftui Nov 24, 2021
@maxep maxep deleted the maxep/RUMM-1745/swiftui-uikit-navigation branch November 24, 2021 12:35
@maxep maxep mentioned this pull request Dec 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants