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
This commit is contained in:
Simon Jarbrant 2021-03-14 12:33:01 +01:00 committed by GitHub
parent f246b0715c
commit 461fa7b608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 21 deletions

View File

@ -7,6 +7,7 @@ Changelog
- Add Github Action - Add Github Action
- Added `.introspectTextView()`. - Added `.introspectTextView()`.
- Update CircleCI config to use Xcode 12.4.0 - Update CircleCI config to use Xcode 12.4.0
- Fixed nested `ScrollView` detection on iOS 14 and macOS 11
## [0.1.2] ## [0.1.2]

View File

@ -244,6 +244,13 @@ public enum TargetViewSelector {
} }
return Introspect.previousSibling(containing: TargetView.self, from: viewHost) return Introspect.previousSibling(containing: TargetView.self, from: viewHost)
} }
public static func siblingContainingOrAncestor<TargetView: PlatformView>(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<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? { public static func siblingOfType<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? {
guard let viewHost = Introspect.findViewHost(from: entry) else { guard let viewHost = Introspect.findViewHost(from: entry) else {
@ -251,7 +258,14 @@ public enum TargetViewSelector {
} }
return Introspect.previousSibling(ofType: TargetView.self, from: viewHost) return Introspect.previousSibling(ofType: TargetView.self, from: viewHost)
} }
public static func siblingOfTypeOrAncestor<TargetView: PlatformView>(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<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? { public static func ancestorOrSiblingContaining<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? {
if let tableView = Introspect.findAncestor(ofType: TargetView.self, from: entry) { if let tableView = Introspect.findAncestor(ofType: TargetView.self, from: entry) {
return tableView return tableView

View File

@ -83,9 +83,9 @@ extension View {
/// Finds a `UIScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child. /// Finds a `UIScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child.
public func introspectScrollView(customize: @escaping (UIScrollView) -> ()) -> some View { public func introspectScrollView(customize: @escaping (UIScrollView) -> ()) -> some View {
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) { 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 { } 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. /// Finds a `NSScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child.
public func introspectScrollView(customize: @escaping (NSScrollView) -> ()) -> some View { 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` /// Finds a `NSTextField` from a `SwiftUI.TextField`

View File

@ -55,8 +55,8 @@ private struct ListTestView: View {
@available(macOS 10.15.0, *) @available(macOS 10.15.0, *)
private struct ScrollTestView: View { private struct ScrollTestView: View {
let spy1: () -> Void let spy1: (NSScrollView) -> Void
let spy2: () -> Void let spy2: (NSScrollView) -> Void
var body: some View { var body: some View {
HStack { HStack {
@ -64,13 +64,39 @@ private struct ScrollTestView: View {
Text("Item 1") Text("Item 1")
} }
.introspectScrollView { scrollView in .introspectScrollView { scrollView in
self.spy1() self.spy1(scrollView)
} }
ScrollView { ScrollView {
Text("Item 1") Text("Item 1")
.introspectScrollView { scrollView in .introspectScrollView { scrollView in
self.spy2() 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(scrollView)
}
}
.introspectScrollView { scrollView in
self.spy1(scrollView)
} }
} }
} }
@ -175,18 +201,59 @@ class AppKitTests: XCTestCase {
wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout) wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout)
} }
func testScrollView() { func testScrollView() throws {
let expectation1 = XCTestExpectation() let expectation1 = XCTestExpectation()
let expectation2 = XCTestExpectation() let expectation2 = XCTestExpectation()
var scrollView1: NSScrollView?
var scrollView2: NSScrollView?
let view = ScrollTestView( let view = ScrollTestView(
spy1: { expectation1.fulfill() }, spy1: { scrollView in
spy2: { expectation2.fulfill() } scrollView1 = scrollView
expectation1.fulfill() },
spy2: { scrollView in
scrollView2 = scrollView
expectation2.fulfill()
}
) )
TestUtils.present(view: view) TestUtils.present(view: view)
wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) 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() { func testTextField() {
let expectation = XCTestExpectation() let expectation = XCTestExpectation()

View File

@ -139,8 +139,8 @@ private struct ListTestView: View {
@available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *) @available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *)
private struct ScrollTestView: View { private struct ScrollTestView: View {
let spy1: () -> Void let spy1: (UIScrollView) -> Void
let spy2: () -> Void let spy2: (UIScrollView) -> Void
var body: some View { var body: some View {
HStack { HStack {
@ -148,18 +148,43 @@ private struct ScrollTestView: View {
Text("Item 1") Text("Item 1")
} }
.introspectScrollView { scrollView in .introspectScrollView { scrollView in
self.spy1() self.spy1(scrollView)
} }
ScrollView { ScrollView {
Text("Item 1") Text("Item 1")
.introspectScrollView { scrollView in .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, *) @available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *)
private struct TextFieldTestView: View { private struct TextFieldTestView: View {
let spy: () -> Void let spy: () -> Void
@ -316,18 +341,60 @@ class UIKitTests: XCTestCase {
wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout) wait(for: [expectation1, expectation2, cellExpectation1, cellExpectation2], timeout: TestUtils.Constants.timeout)
} }
func testScrollView() { func testScrollView() throws {
let expectation1 = XCTestExpectation() let expectation1 = XCTestExpectation()
let expectation2 = XCTestExpectation() let expectation2 = XCTestExpectation()
var scrollView1: UIScrollView?
var scrollView2: UIScrollView?
let view = ScrollTestView( let view = ScrollTestView(
spy1: { expectation1.fulfill() }, spy1: { scrollView in
spy2: { expectation2.fulfill() } scrollView1 = scrollView
expectation1.fulfill()
},
spy2: { scrollView in
scrollView2 = scrollView
expectation2.fulfill()
}
) )
TestUtils.present(view: view) TestUtils.present(view: view)
wait(for: [expectation1, expectation2], timeout: TestUtils.Constants.timeout) 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() { func testTextField() {
let expectation = XCTestExpectation() let expectation = XCTestExpectation()