Skip to content

Commit 725a292

Browse files
pilot34Gleb Tarasov
andauthored
Extract isTopScreen helper function for public use (#21)
Co-authored-by: Gleb Tarasov <[email protected]>
1 parent a4cf9bd commit 725a292

File tree

3 files changed

+167
-89
lines changed

3 files changed

+167
-89
lines changed

PerformanceSuite/Sources/Observers/LastOpenedScreenObserver.swift

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class LastOpenedScreenObserver: ViewControllerObserver {
2626

2727
private func rememberOpenedScreenIfNeeded(_ viewController: UIViewController) {
2828
DispatchQueue.main.async {
29-
guard self.isTopScreen(viewController) else {
29+
guard isTopScreen(viewController) else {
3030
return
3131
}
3232
PerformanceMonitoring.queue.async {
@@ -35,92 +35,4 @@ final class LastOpenedScreenObserver: ViewControllerObserver {
3535
}
3636
}
3737
}
38-
39-
private func isTopScreen(_ viewController: UIViewController) -> Bool {
40-
assert(Thread.isMainThread)
41-
42-
// if our class is a container - skip it
43-
if isContainerController(viewController) {
44-
return false
45-
}
46-
47-
// if we have a parent, which is not a container controller -> this is not a top screen
48-
if let parent = viewController.parent {
49-
if !isContainerController(parent) {
50-
return false
51-
}
52-
}
53-
54-
// skip all UIKit controllers
55-
if isUIKitController(viewController) {
56-
return false
57-
}
58-
59-
// this is a controller inside a cell
60-
if isCellSubview(viewController.view) || isCellSubview(viewController.view.superview) {
61-
return false
62-
}
63-
64-
// there are UIHostingControllers used for navigation bar buttons. Ignore them too
65-
if isNavigationBarSubview(viewController.view) {
66-
return false
67-
}
68-
69-
return true
70-
}
71-
72-
private func isUIKitController(_ viewController: UIViewController) -> Bool {
73-
// we do not consider UIHostingController as UIKit controller,
74-
// because inside it contains our custom SwiftUI views
75-
if viewController is HostingControllerIdentifier {
76-
return false
77-
}
78-
let viewControllerBundle = Bundle(for: type(of: viewController))
79-
return viewControllerBundle == uiKitBundle
80-
}
81-
82-
private func isContainerController(_ viewController: UIViewController) -> Bool {
83-
let vcType = type(of: viewController)
84-
return uiKitContainers.contains {
85-
vcType.isSubclass(of: $0)
86-
}
87-
}
88-
89-
private func isCellSubview(_ view: UIView?) -> Bool {
90-
guard let view = view else {
91-
return false
92-
}
93-
94-
if view.superview is UITableViewCell {
95-
return true
96-
}
97-
98-
return false
99-
}
100-
101-
private func isNavigationBarSubview(_ view: UIView?) -> Bool {
102-
if view == nil {
103-
return false
104-
}
105-
106-
if view is UINavigationBar {
107-
return true
108-
}
109-
110-
return isNavigationBarSubview(view?.superview)
111-
}
112-
113-
private lazy var uiKitBundle = Bundle(for: UIViewController.self)
114-
private lazy var uiKitContainers = [
115-
UINavigationController.self,
116-
UITabBarController.self,
117-
UISplitViewController.self,
118-
UIPageViewController.self,
119-
]
120-
12138
}
122-
123-
// We cannot check `viewController is UIHostingController` because of generics,
124-
// so we use helper protocol here
125-
protocol HostingControllerIdentifier { }
126-
extension UIHostingController: HostingControllerIdentifier { }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// TopScreen.swift
3+
// Pods
4+
//
5+
// Created by Gleb Tarasov on 5/1/25.
6+
//
7+
8+
import SwiftUI
9+
import UIKit
10+
11+
/// Determines if the given view controller is the top screen in the view hierarchy.
12+
///
13+
/// This function checks several conditions to determine if the specified `UIViewController`
14+
/// is considered the top screen. It asserts that the function is called on the main thread.
15+
/// The function returns `false` if:
16+
/// - The view controller is a container controller.
17+
/// - The view controller has a parent that is not a container controller.
18+
/// - The view controller is a UIKit controller.
19+
/// - The view controller is inside a cell.
20+
/// - The view controller is used for navigation bar buttons.
21+
///
22+
/// - Parameter viewController: The `UIViewController` to check.
23+
/// - Returns: `true` if the view controller is the top screen, `false` otherwise.
24+
public func isTopScreen(_ viewController: UIViewController) -> Bool {
25+
assert(Thread.isMainThread)
26+
27+
// if our class is a container - skip it
28+
if isContainerController(viewController) {
29+
return false
30+
}
31+
32+
// if we have a parent, which is not a container controller -> this is not a top screen
33+
if let parent = viewController.parent {
34+
if !isContainerController(parent) {
35+
return false
36+
}
37+
}
38+
39+
// skip all UIKit controllers
40+
if isUIKitController(viewController) {
41+
return false
42+
}
43+
44+
// this is a controller inside a cell
45+
if isCellSubview(viewController.view) || isCellSubview(viewController.view.superview) {
46+
return false
47+
}
48+
49+
// there are UIHostingControllers used for navigation bar buttons. Ignore them too
50+
if isNavigationBarSubview(viewController.view) {
51+
return false
52+
}
53+
54+
return true
55+
}
56+
57+
private func isUIKitController(_ viewController: UIViewController) -> Bool {
58+
// we do not consider UIHostingController as UIKit controller,
59+
// because inside it contains our custom SwiftUI views
60+
if viewController is HostingControllerIdentifier {
61+
return false
62+
}
63+
let viewControllerBundle = Bundle(for: type(of: viewController))
64+
return viewControllerBundle == uiKitBundle
65+
}
66+
67+
private func isContainerController(_ viewController: UIViewController) -> Bool {
68+
let vcType = type(of: viewController)
69+
return uiKitContainers.contains {
70+
vcType.isSubclass(of: $0)
71+
}
72+
}
73+
74+
private func isCellSubview(_ view: UIView?) -> Bool {
75+
guard let view = view else {
76+
return false
77+
}
78+
79+
if view.superview is UITableViewCell {
80+
return true
81+
}
82+
83+
return false
84+
}
85+
86+
private func isNavigationBarSubview(_ view: UIView?) -> Bool {
87+
if view == nil {
88+
return false
89+
}
90+
91+
if view is UINavigationBar {
92+
return true
93+
}
94+
95+
return isNavigationBarSubview(view?.superview)
96+
}
97+
98+
private let uiKitBundle = Bundle(for: UIViewController.self)
99+
private let uiKitContainers = [
100+
UINavigationController.self,
101+
UITabBarController.self,
102+
UISplitViewController.self,
103+
UIPageViewController.self,
104+
]
105+
106+
// We cannot check `viewController is UIHostingController` because of generics,
107+
// so we use helper protocol here
108+
protocol HostingControllerIdentifier { }
109+
extension UIHostingController: HostingControllerIdentifier { }
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import XCTest
2+
import UIKit
3+
import SwiftUI
4+
@testable import PerformanceSuite
5+
6+
class TopScreenTests: XCTestCase {
7+
8+
func testIsTopScreenWithNavigationParent() {
9+
let myController = MyViewController()
10+
let nav = UINavigationController(rootViewController: myController)
11+
XCTAssertTrue(isTopScreen(myController))
12+
XCTAssertFalse(isTopScreen(nav))
13+
}
14+
15+
func testIsTopScreenWithNonContainerParent() {
16+
let parentController = MyViewController()
17+
let childController = MyViewController()
18+
parentController.addChild(childController)
19+
XCTAssertFalse(isTopScreen(childController))
20+
XCTAssertTrue(isTopScreen(parentController))
21+
}
22+
23+
func testIsTopScreenWithCellSubview() {
24+
let cellSubviewController = MyViewController()
25+
let cellView = UITableViewCell()
26+
cellView.contentView.addSubview(cellSubviewController.view)
27+
XCTAssertFalse(isTopScreen(cellSubviewController))
28+
}
29+
30+
func testIsTopScreenWithNavigationBarSubview() {
31+
let navBarSubviewController = MyViewController()
32+
let navBarView = UINavigationBar()
33+
navBarView.addSubview(navBarSubviewController.view)
34+
XCTAssertFalse(isTopScreen(navBarSubviewController))
35+
}
36+
37+
func testIsTopScreenWithValidTopScreen() {
38+
let top = MyViewController()
39+
XCTAssertTrue(isTopScreen(top))
40+
}
41+
42+
func testIsTopWithUIKitController() {
43+
let top = UIViewController()
44+
XCTAssertFalse(isTopScreen(top))
45+
}
46+
47+
func testIsTopWithHostingController() {
48+
let view = MyViewForLastScreenObserverTests()
49+
let hosting = UIHostingController(rootView: view)
50+
XCTAssertTrue(isTopScreen(hosting))
51+
52+
let nav = UINavigationController(rootViewController: hosting)
53+
XCTAssertTrue(isTopScreen(hosting))
54+
}
55+
}
56+
57+
private class MyViewController: UIViewController { }

0 commit comments

Comments
 (0)