From 461fa7b608238c03b07008a41372ed392b319c8c Mon Sep 17 00:00:00 2001 From: Simon Jarbrant Date: Sun, 14 Mar 2021 12:33:01 +0100 Subject: [PATCH] Fix an issue where nested ScrollViews would not be detected properly (#69) * Fix an issue where nested ScrollViews would not be detected properly * Fix nested ScrollView issue on iOS 13 * Clean up implementation of siblingOrAncestorOfType to avoid extraneous searches * Adjust naming for correctness * Add nested ScrollView tests for macOS This also fixes the same issue iOS had, where nested ScrollViews would not be correctly detected on macOS 11+ * Update UIKit ScrollView tests to match AppKit ones * Add changelog entry for nested scrollview fixes * Change NSScrollView lookup mechanism for macOS 10.15 to match UIKit behavior --- CHANGELOG.md | 1 + Introspect/Introspect.swift | 16 +++++- Introspect/ViewExtensions.swift | 10 ++-- IntrospectTests/AppKitTests.swift | 83 ++++++++++++++++++++++++++++--- IntrospectTests/UIKitTests.swift | 83 ++++++++++++++++++++++++++++--- 5 files changed, 173 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db653c8..f2d259d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Changelog - Add Github Action - Added `.introspectTextView()`. - Update CircleCI config to use Xcode 12.4.0 +- Fixed nested `ScrollView` detection on iOS 14 and macOS 11 ## [0.1.2] diff --git a/Introspect/Introspect.swift b/Introspect/Introspect.swift index 5673ec2e..092e6ea7 100644 --- a/Introspect/Introspect.swift +++ b/Introspect/Introspect.swift @@ -244,6 +244,13 @@ public enum TargetViewSelector { } return Introspect.previousSibling(containing: TargetView.self, from: viewHost) } + + public static func siblingContainingOrAncestor(from entry: PlatformView) -> TargetView? { + if let sibling: TargetView = siblingContaining(from: entry) { + return sibling + } + return Introspect.findAncestor(ofType: TargetView.self, from: entry) + } public static func siblingOfType(from entry: PlatformView) -> TargetView? { guard let viewHost = Introspect.findViewHost(from: entry) else { @@ -251,7 +258,14 @@ public enum TargetViewSelector { } return Introspect.previousSibling(ofType: TargetView.self, from: viewHost) } - + + public static func siblingOfTypeOrAncestor(from entry: PlatformView) -> TargetView? { + if let sibling: TargetView = siblingOfType(from: entry) { + return sibling + } + return Introspect.findAncestor(ofType: TargetView.self, from: entry) + } + public static func ancestorOrSiblingContaining(from entry: PlatformView) -> TargetView? { if let tableView = Introspect.findAncestor(ofType: TargetView.self, from: entry) { return tableView diff --git a/Introspect/ViewExtensions.swift b/Introspect/ViewExtensions.swift index 9dd9b105..314d12a9 100644 --- a/Introspect/ViewExtensions.swift +++ b/Introspect/ViewExtensions.swift @@ -83,9 +83,9 @@ extension View { /// Finds a `UIScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child. public func introspectScrollView(customize: @escaping (UIScrollView) -> ()) -> some View { if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) { - return introspect(selector: TargetViewSelector.ancestorOrSiblingOfType, customize: customize) + return introspect(selector: TargetViewSelector.siblingOfTypeOrAncestor, customize: customize) } else { - return introspect(selector: TargetViewSelector.ancestorOrSiblingContaining, customize: customize) + return introspect(selector: TargetViewSelector.siblingContainingOrAncestor, customize: customize) } } @@ -157,7 +157,11 @@ extension View { /// Finds a `NSScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child. public func introspectScrollView(customize: @escaping (NSScrollView) -> ()) -> some View { - return introspect(selector: TargetViewSelector.ancestorOrSiblingContaining, customize: customize) + if #available(macOS 11.0, *) { + return introspect(selector: TargetViewSelector.siblingOfTypeOrAncestor, customize: customize) + } else { + return introspect(selector: TargetViewSelector.siblingContainingOrAncestor, customize: customize) + } } /// Finds a `NSTextField` from a `SwiftUI.TextField` diff --git a/IntrospectTests/AppKitTests.swift b/IntrospectTests/AppKitTests.swift index 84f09909..0d2ce28c 100644 --- a/IntrospectTests/AppKitTests.swift +++ b/IntrospectTests/AppKitTests.swift @@ -55,8 +55,8 @@ private struct ListTestView: View { @available(macOS 10.15.0, *) private struct ScrollTestView: View { - let spy1: () -> Void - let spy2: () -> Void + let spy1: (NSScrollView) -> Void + let spy2: (NSScrollView) -> Void var body: some View { HStack { @@ -64,14 +64,40 @@ private struct ScrollTestView: View { Text("Item 1") } .introspectScrollView { scrollView in - self.spy1() + self.spy1(scrollView) } + ScrollView { Text("Item 1") + .introspectScrollView { scrollView in + self.spy2(scrollView) + } + } + } + } +} + +@available(macOS 10.15.0, *) +private struct NestedScrollTestView: View { + + let spy1: (NSScrollView) -> Void + let spy2: (NSScrollView) -> Void + + var body: some View { + HStack { + ScrollView { + Text("Item 1") + + ScrollView { + Text("Item 1") + } .introspectScrollView { scrollView in - self.spy2() + self.spy2(scrollView) } } + .introspectScrollView { scrollView in + self.spy1(scrollView) + } } } } @@ -175,18 +201,59 @@ class AppKitTests: XCTestCase { wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout) } - func testScrollView() { + func testScrollView() throws { let expectation1 = XCTestExpectation() let expectation2 = XCTestExpectation() + + var scrollView1: NSScrollView? + var scrollView2: NSScrollView? + let view = ScrollTestView( - spy1: { expectation1.fulfill() }, - spy2: { expectation2.fulfill() } + spy1: { scrollView in + scrollView1 = scrollView + expectation1.fulfill() }, + spy2: { scrollView in + scrollView2 = scrollView + expectation2.fulfill() + } ) TestUtils.present(view: view) wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) + + let unwrappedScrollView1 = try XCTUnwrap(scrollView1) + let unwrappedScrollView2 = try XCTUnwrap(scrollView2) + + XCTAssertNotEqual(unwrappedScrollView1, unwrappedScrollView2) } - + + func testNestedScrollView() throws { + + let expectation1 = XCTestExpectation() + let expectation2 = XCTestExpectation() + + var scrollView1: NSScrollView? + var scrollView2: NSScrollView? + + let view = NestedScrollTestView( + spy1: { scrollView in + scrollView1 = scrollView + expectation1.fulfill() + }, + spy2: { scrollView in + scrollView2 = scrollView + expectation2.fulfill() + } + ) + TestUtils.present(view: view) + wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) + + let unwrappedScrollView1 = try XCTUnwrap(scrollView1) + let unwrappedScrollView2 = try XCTUnwrap(scrollView2) + + XCTAssertNotEqual(unwrappedScrollView1, unwrappedScrollView2) + } + func testTextField() { let expectation = XCTestExpectation() diff --git a/IntrospectTests/UIKitTests.swift b/IntrospectTests/UIKitTests.swift index 66470ba2..693ccc08 100644 --- a/IntrospectTests/UIKitTests.swift +++ b/IntrospectTests/UIKitTests.swift @@ -139,8 +139,8 @@ private struct ListTestView: View { @available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *) private struct ScrollTestView: View { - let spy1: () -> Void - let spy2: () -> Void + let spy1: (UIScrollView) -> Void + let spy2: (UIScrollView) -> Void var body: some View { HStack { @@ -148,18 +148,43 @@ private struct ScrollTestView: View { Text("Item 1") } .introspectScrollView { scrollView in - self.spy1() + self.spy1(scrollView) } ScrollView { Text("Item 1") .introspectScrollView { scrollView in - self.spy2() + self.spy2(scrollView) } } } } } +@available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *) +private struct NestedScrollTestView: View { + + let spy1: (UIScrollView) -> Void + let spy2: (UIScrollView) -> Void + + var body: some View { + HStack { + ScrollView(showsIndicators: true) { + Text("Item 1") + + ScrollView(showsIndicators: false) { + Text("Item 1") + } + .introspectScrollView { scrollView in + self.spy2(scrollView) + } + } + .introspectScrollView { scrollView in + self.spy1(scrollView) + } + } + } +} + @available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *) private struct TextFieldTestView: View { let spy: () -> Void @@ -316,18 +341,60 @@ class UIKitTests: XCTestCase { wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout) } - func testScrollView() { + func testScrollView() throws { let expectation1 = XCTestExpectation() let expectation2 = XCTestExpectation() + + var scrollView1: UIScrollView? + var scrollView2: UIScrollView? + let view = ScrollTestView( - spy1: { expectation1.fulfill() }, - spy2: { expectation2.fulfill() } + spy1: { scrollView in + scrollView1 = scrollView + expectation1.fulfill() + }, + spy2: { scrollView in + scrollView2 = scrollView + expectation2.fulfill() + } ) TestUtils.present(view: view) wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) + + let unwrappedScrollView1 = try XCTUnwrap(scrollView1) + let unwrappedScrollView2 = try XCTUnwrap(scrollView2) + + XCTAssertNotEqual(unwrappedScrollView1, unwrappedScrollView2) } - + + func testNestedScrollView() throws { + + let expectation1 = XCTestExpectation() + let expectation2 = XCTestExpectation() + + var scrollView1: UIScrollView? + var scrollView2: UIScrollView? + + let view = NestedScrollTestView( + spy1: { scrollView in + scrollView1 = scrollView + expectation1.fulfill() + }, + spy2: { scrollView in + scrollView2 = scrollView + expectation2.fulfill() + } + ) + TestUtils.present(view: view) + wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) + + let unwrappedScrollView1 = try XCTUnwrap(scrollView1) + let unwrappedScrollView2 = try XCTUnwrap(scrollView2) + + XCTAssertNotEqual(unwrappedScrollView1, unwrappedScrollView2) + } + func testTextField() { let expectation = XCTestExpectation()