From ab071ef915210a05095fa4cf01261ee12e0e43a6 Mon Sep 17 00:00:00 2001 From: Joe Newton Date: Tue, 21 Sep 2021 15:27:12 -0400 Subject: [PATCH 01/54] Added Inspectability for EllipticalGradient values --- .../SwiftUI/EllipticalGradient.swift | 55 ++++++++++++++++ Sources/ViewInspector/ViewSearchIndex.swift | 6 +- .../SwiftUI/EllipticalGradientTests.swift | 63 +++++++++++++++++++ ViewInspector.xcodeproj/project.pbxproj | 8 +++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 Sources/ViewInspector/SwiftUI/EllipticalGradient.swift create mode 100644 Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift diff --git a/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift b/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift new file mode 100644 index 00000000..b512af91 --- /dev/null +++ b/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift @@ -0,0 +1,55 @@ +import SwiftUI + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +public extension ViewType { + + struct EllipticalGradient: KnownViewType { + public static var typePrefix: String = "EllipticalGradient" + } +} + +// MARK: - Extraction from SingleViewContent parent + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +public extension InspectableView where View: SingleViewContent { + + func ellipticalGradient() throws -> InspectableView { + return try .init(try child(), parent: self) + } +} + +// MARK: - Extraction from MultipleViewContent parent + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +public extension InspectableView where View: MultipleViewContent { + + func ellipticalGradient(_ index: Int) throws -> InspectableView { + return try .init(try child(at: index), parent: self, index: index) + } +} + +// MARK: - Custom Attributes + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +public extension InspectableView where View == ViewType.EllipticalGradient { + + func gradient() throws -> Gradient { + return try Inspector + .attribute(label: "gradient", value: content.view, type: Gradient.self) + } + + func center() throws -> UnitPoint { + return try Inspector + .attribute(label: "center", value: content.view, type: UnitPoint.self) + } + + func startRadiusFraction() throws -> CGFloat { + return try Inspector + .attribute(label: "startRadiusFraction", value: content.view, type: CGFloat.self) + } + + func endRadiusFraction() throws -> CGFloat { + return try Inspector + .attribute(label: "endRadiusFraction", value: content.view, type: CGFloat.self) + } +} diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index c9a4d724..e90b5d8f 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -6,7 +6,7 @@ import SwiftUI internal extension ViewSearch { private static var index: [String: [ViewIdentity]] = { - let identities: [ViewIdentity] = [ + var identities: [ViewIdentity] = [ .init(ViewType.ActionSheet.self), .init(ViewType.Alert.self), .init(ViewType.AlertButton.self), .init(ViewType.AngularGradient.self), .init(ViewType.AnyView.self), @@ -51,6 +51,10 @@ internal extension ViewSearch { .init(ViewType.ViewModifierContent.self), .init(ViewType.VSplitView.self), .init(ViewType.VStack.self), .init(ViewType.ZStack.self) ] + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + identities.append(.init(ViewType.EllipticalGradient.self)) + } + var index = [String: [ViewIdentity]](minimumCapacity: 26) // alphabet identities.forEach { identity in let names = identity.viewType.namespacedPrefixes diff --git a/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift b/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift new file mode 100644 index 00000000..86644aba --- /dev/null +++ b/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift @@ -0,0 +1,63 @@ +import XCTest +import SwiftUI +@testable import ViewInspector + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +final class EllipticalGradientTests: XCTestCase { + + let gradient = Gradient(colors: [.red]) + + func testInspect() throws { + let sut = EllipticalGradient(gradient: gradient, center: .center) + XCTAssertNoThrow(try sut.inspect()) + } + + func testExtractionFromSingleViewContainer() throws { + let view = AnyView(EllipticalGradient(gradient: gradient, center: .center)) + XCTAssertNoThrow(try view.inspect().anyView().ellipticalGradient()) + } + + func testExtractionFromMultipleViewContainer() throws { + let view = HStack { + EllipticalGradient(gradient: gradient, center: .center) + EllipticalGradient(gradient: gradient, center: .center) + } + XCTAssertNoThrow(try view.inspect().hStack().ellipticalGradient(0)) + XCTAssertNoThrow(try view.inspect().hStack().ellipticalGradient(1)) + } + + func testSearch() throws { + let view = AnyView(EllipticalGradient(gradient: gradient, center: .center)) + XCTAssertEqual(try view.inspect().find(ViewType.EllipticalGradient.self).pathToRoot, + "anyView().ellipticalGradient()") + } + + func testGradient() throws { + let sut = try EllipticalGradient(gradient: gradient, center: .center) + .inspect().ellipticalGradient().gradient() + XCTAssertEqual(sut, gradient) + } + + func testCenter() throws { + let center: UnitPoint = .topLeading + let sut = try EllipticalGradient(gradient: gradient, center: center) + .inspect().ellipticalGradient().center() + XCTAssertEqual(sut, center) + } + + func testStartRadiusFraction() throws { + let radius: CGFloat = 0.5 + let sut = try EllipticalGradient(gradient: gradient, center: .center, + startRadiusFraction: radius, endRadiusFraction: 1.0) + .inspect().ellipticalGradient().startRadiusFraction() + XCTAssertEqual(sut, radius) + } + + func testEndAngle() throws { + let radius: CGFloat = 0.5 + let sut = try EllipticalGradient(gradient: gradient, center: .center, + startRadiusFraction: 0.0, endRadiusFraction: radius) + .inspect().ellipticalGradient().endRadiusFraction() + XCTAssertEqual(sut, radius) + } +} diff --git a/ViewInspector.xcodeproj/project.pbxproj b/ViewInspector.xcodeproj/project.pbxproj index d94d651f..f46b094b 100644 --- a/ViewInspector.xcodeproj/project.pbxproj +++ b/ViewInspector.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ CDA84516262CC1F800C56C98 /* CommonComposedGestureUpdatingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA84515262CC1F800C56C98 /* CommonComposedGestureUpdatingTests.swift */; }; CDA8451C262CC34D00C56C98 /* CommonComposedGestureChangedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA8451B262CC34D00C56C98 /* CommonComposedGestureChangedTests.swift */; }; D766E67026E17F01004AAA80 /* FullScreenCoverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D766E66F26E17F01004AAA80 /* FullScreenCoverTests.swift */; }; + DD421B5F26FA6648008EFE05 /* EllipticalGradientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD421B5E26FA6648008EFE05 /* EllipticalGradientTests.swift */; }; + DDCA44AD26FA64F500138898 /* EllipticalGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCA44AC26FA64F500138898 /* EllipticalGradient.swift */; }; F6026A27256A7D1900CA31E5 /* TextAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6026A26256A7D1900CA31E5 /* TextAttributes.swift */; }; F6026A31256A7E3D00CA31E5 /* TextAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6026A30256A7E3D00CA31E5 /* TextAttributesTests.swift */; }; F60385D523D3C74B008F31BD /* InspectionEmissary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60385D423D3C74B008F31BD /* InspectionEmissary.swift */; }; @@ -330,6 +332,8 @@ CDA84515262CC1F800C56C98 /* CommonComposedGestureUpdatingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonComposedGestureUpdatingTests.swift; sourceTree = ""; }; CDA8451B262CC34D00C56C98 /* CommonComposedGestureChangedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonComposedGestureChangedTests.swift; sourceTree = ""; }; D766E66F26E17F01004AAA80 /* FullScreenCoverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCoverTests.swift; sourceTree = ""; }; + DD421B5E26FA6648008EFE05 /* EllipticalGradientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EllipticalGradientTests.swift; sourceTree = ""; }; + DDCA44AC26FA64F500138898 /* EllipticalGradient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EllipticalGradient.swift; sourceTree = ""; }; F6026A26256A7D1900CA31E5 /* TextAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttributes.swift; sourceTree = ""; }; F6026A30256A7E3D00CA31E5 /* TextAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttributesTests.swift; sourceTree = ""; }; F60385D423D3C74B008F31BD /* InspectionEmissary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectionEmissary.swift; sourceTree = ""; }; @@ -621,6 +625,7 @@ F682A015254776F2005F1B70 /* DisclosureGroup.swift */, F60EEBC72382EED0007DB53A /* Divider.swift */, F6DFD87B2382FB510028E84D /* EditButton.swift */, + DDCA44AC26FA64F500138898 /* EllipticalGradient.swift */, F64057EE238DBA800029D9BA /* EmptyView.swift */, F6F08E6C23A2A9D4001F04DF /* EnvironmentReaderView.swift */, F6D933A62385E9E000358E0E /* EquatableView.swift */, @@ -732,6 +737,7 @@ F682A00B254772C3005F1B70 /* DisclosureGroupTests.swift */, F60EEBEC2382F004007DB53A /* DividerTests.swift */, F6DFD87D2382FBE80028E84D /* EditButtonTests.swift */, + DD421B5E26FA6648008EFE05 /* EllipticalGradientTests.swift */, F6D933A82385E9F700358E0E /* EquatableViewTests.swift */, F64057F0238DBB120029D9BA /* EmptyViewTests.swift */, F6F08E6E23A2AA64001F04DF /* EnvironmentReaderViewTests.swift */, @@ -1094,6 +1100,7 @@ F6D9339F2385B46A00358E0E /* NavigationLink.swift in Sources */, F6ECF6D423A68BA4000FC591 /* PositioningModifiers.swift in Sources */, F6DB5A01253510350056FC83 /* TextInputModifiers.swift in Sources */, + DDCA44AD26FA64F500138898 /* EllipticalGradient.swift in Sources */, F60EEBD72382EED0007DB53A /* Form.swift in Sources */, F6C2E4D623AB214A00C7308F /* VisualEffectModifiers.swift in Sources */, F60EEBDD2382EED0007DB53A /* Button.swift in Sources */, @@ -1124,6 +1131,7 @@ F60EEBFD2382F004007DB53A /* ImageTests.swift in Sources */, F60EEBFE2382F004007DB53A /* FormTests.swift in Sources */, F6DFD87E2382FBE80028E84D /* EditButtonTests.swift in Sources */, + DD421B5F26FA6648008EFE05 /* EllipticalGradientTests.swift in Sources */, F60385D723D3CDED008F31BD /* InspectionEmissaryTests.swift in Sources */, CDA844F2262B86D900C56C98 /* GestureExampleTests.swift in Sources */, F6D933C92385FC1A00358E0E /* StepperTests.swift in Sources */, From c7821fa3406f90bb151a521a67681d710aade5fa Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Sep 2021 23:44:09 +0300 Subject: [PATCH 02/54] Clean up after the merge --- .watchOS/watchOS.xcodeproj/project.pbxproj | 6 ++++++ Sources/ViewInspector/SwiftUI/EllipticalGradient.swift | 2 +- Sources/ViewInspector/ViewSearchIndex.swift | 6 ++---- .../SwiftUI/EllipticalGradientTests.swift | 10 +++++++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.watchOS/watchOS.xcodeproj/project.pbxproj b/.watchOS/watchOS.xcodeproj/project.pbxproj index 96f546fb..9bd8fbde 100644 --- a/.watchOS/watchOS.xcodeproj/project.pbxproj +++ b/.watchOS/watchOS.xcodeproj/project.pbxproj @@ -257,6 +257,8 @@ 520DC70C26F60FF400FCFFFD /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 520DC68E26F60FF400FCFFFD /* ViewInspector */; }; 520DC70E26F60FF400FCFFFD /* Test.strings in Resources */ = {isa = PBXBuildFile; fileRef = 520DC5AA26F60FBA00FCFFFD /* Test.strings */; }; 520DC71526F6115D00FCFFFD /* watchOSApp+Testable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CA301526ECD64400BFD568 /* watchOSApp+Testable.swift */; }; + 5220F52326FA7AF2006ECD9F /* EllipticalGradientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5220F52226FA7AF2006ECD9F /* EllipticalGradientTests.swift */; }; + 5220F52426FA7AF2006ECD9F /* EllipticalGradientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5220F52226FA7AF2006ECD9F /* EllipticalGradientTests.swift */; }; 5293010626EFC96600012E90 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5293010426EFC96600012E90 /* Interface.storyboard */; }; 5293010E26EFC96700012E90 /* watchOS-Ext-2.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5293010D26EFC96700012E90 /* watchOS-Ext-2.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5293012726EFCBD300012E90 /* watchOSApp-2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5293011426EFC96700012E90 /* watchOSApp-2.swift */; }; @@ -458,6 +460,7 @@ 520DC60D26F60FBA00FCFFFD /* ViewPaddingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewPaddingTests.swift; sourceTree = ""; }; 520DC60E26F60FBA00FCFFFD /* TransformingModifiersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformingModifiersTests.swift; sourceTree = ""; }; 520DC71226F60FF400FCFFFD /* watchOS-2-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "watchOS-2-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5220F52226FA7AF2006ECD9F /* EllipticalGradientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EllipticalGradientTests.swift; sourceTree = ""; }; 529300FF26EFC96600012E90 /* watchOS-App-2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "watchOS-App-2.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5293010526EFC96600012E90 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; 5293010D26EFC96700012E90 /* watchOS-Ext-2.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "watchOS-Ext-2.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -582,6 +585,7 @@ 520DC5B026F60FBA00FCFFFD /* SwiftUI */ = { isa = PBXGroup; children = ( + 5220F52226FA7AF2006ECD9F /* EllipticalGradientTests.swift */, 520DC5B126F60FBA00FCFFFD /* ZStackTests.swift */, 520DC5B226F60FBA00FCFFFD /* OptionalViewTests.swift */, 520DC5B326F60FBA00FCFFFD /* MenuButtonTests.swift */, @@ -973,6 +977,7 @@ 520DC62C26F60FBB00FCFFFD /* InspectableViewTests.swift in Sources */, 520DC61E26F60FBA00FCFFFD /* ExclusiveGestureTests.swift in Sources */, 520DC63026F60FBB00FCFFFD /* MenuButtonTests.swift in Sources */, + 5220F52326FA7AF2006ECD9F /* EllipticalGradientTests.swift in Sources */, 520DC64426F60FBB00FCFFFD /* EnvironmentObjectInjectionTests.swift in Sources */, 520DC67A26F60FBB00FCFFFD /* LazyVStackTests.swift in Sources */, 520DC65E26F60FBB00FCFFFD /* MapAnnotationTests.swift in Sources */, @@ -1103,6 +1108,7 @@ 520DC69126F60FF400FCFFFD /* InspectableViewTests.swift in Sources */, 520DC69226F60FF400FCFFFD /* ExclusiveGestureTests.swift in Sources */, 520DC69326F60FF400FCFFFD /* MenuButtonTests.swift in Sources */, + 5220F52426FA7AF2006ECD9F /* EllipticalGradientTests.swift in Sources */, 520DC69426F60FF400FCFFFD /* EnvironmentObjectInjectionTests.swift in Sources */, 520DC69526F60FF400FCFFFD /* LazyVStackTests.swift in Sources */, 520DC69626F60FF400FCFFFD /* MapAnnotationTests.swift in Sources */, diff --git a/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift b/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift index b512af91..0b5db7ba 100644 --- a/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift +++ b/Sources/ViewInspector/SwiftUI/EllipticalGradient.swift @@ -1,6 +1,6 @@ import SwiftUI -@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension ViewType { struct EllipticalGradient: KnownViewType { diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index e90b5d8f..ae3ed75f 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -6,7 +6,7 @@ import SwiftUI internal extension ViewSearch { private static var index: [String: [ViewIdentity]] = { - var identities: [ViewIdentity] = [ + let identities: [ViewIdentity] = [ .init(ViewType.ActionSheet.self), .init(ViewType.Alert.self), .init(ViewType.AlertButton.self), .init(ViewType.AngularGradient.self), .init(ViewType.AnyView.self), @@ -16,6 +16,7 @@ internal extension ViewSearch { .init(ViewType.DatePicker.self), .init(ViewType.DisclosureGroup.self), .init(ViewType.Divider.self), .init(ViewType.EditButton.self), .init(ViewType.EmptyView.self), + .init(ViewType.EllipticalGradient.self), .init(ViewType.ForEach.self), .init(ViewType.Form.self), .init(ViewType.GeometryReader.self), .init(ViewType.Group.self), .init(ViewType.GroupBox.self), @@ -51,9 +52,6 @@ internal extension ViewSearch { .init(ViewType.ViewModifierContent.self), .init(ViewType.VSplitView.self), .init(ViewType.VStack.self), .init(ViewType.ZStack.self) ] - if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { - identities.append(.init(ViewType.EllipticalGradient.self)) - } var index = [String: [ViewIdentity]](minimumCapacity: 26) // alphabet identities.forEach { identity in diff --git a/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift b/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift index 86644aba..da681fb5 100644 --- a/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/EllipticalGradientTests.swift @@ -2,22 +2,25 @@ import XCTest import SwiftUI @testable import ViewInspector -@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) final class EllipticalGradientTests: XCTestCase { let gradient = Gradient(colors: [.red]) func testInspect() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let sut = EllipticalGradient(gradient: gradient, center: .center) XCTAssertNoThrow(try sut.inspect()) } func testExtractionFromSingleViewContainer() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let view = AnyView(EllipticalGradient(gradient: gradient, center: .center)) XCTAssertNoThrow(try view.inspect().anyView().ellipticalGradient()) } func testExtractionFromMultipleViewContainer() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let view = HStack { EllipticalGradient(gradient: gradient, center: .center) EllipticalGradient(gradient: gradient, center: .center) @@ -27,18 +30,21 @@ final class EllipticalGradientTests: XCTestCase { } func testSearch() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let view = AnyView(EllipticalGradient(gradient: gradient, center: .center)) XCTAssertEqual(try view.inspect().find(ViewType.EllipticalGradient.self).pathToRoot, "anyView().ellipticalGradient()") } func testGradient() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let sut = try EllipticalGradient(gradient: gradient, center: .center) .inspect().ellipticalGradient().gradient() XCTAssertEqual(sut, gradient) } func testCenter() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let center: UnitPoint = .topLeading let sut = try EllipticalGradient(gradient: gradient, center: center) .inspect().ellipticalGradient().center() @@ -46,6 +52,7 @@ final class EllipticalGradientTests: XCTestCase { } func testStartRadiusFraction() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let radius: CGFloat = 0.5 let sut = try EllipticalGradient(gradient: gradient, center: .center, startRadiusFraction: radius, endRadiusFraction: 1.0) @@ -54,6 +61,7 @@ final class EllipticalGradientTests: XCTestCase { } func testEndAngle() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let radius: CGFloat = 0.5 let sut = try EllipticalGradient(gradient: gradient, center: .center, startRadiusFraction: 0.0, endRadiusFraction: radius) From c93f49268daf939c89a7465d3226c50ea4e057a9 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Sep 2021 00:03:12 +0300 Subject: [PATCH 03/54] Add EllipticalGradient to readiness --- readiness.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readiness.md b/readiness.md index 2a7c7009..0aa839c7 100644 --- a/readiness.md +++ b/readiness.md @@ -37,6 +37,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| DisclosureGroup | `contained view`, `label view`, `isExpanded: Bool`, `expand()`, `collapse()` | |:white_check_mark:| Divider | | |:white_check_mark:| EditButton | `editMode: Binding?` | +|:white_check_mark:| EllipticalGradient | `gradient: Gradient`, `center: UnitPoint`, `startRadiusFraction: CGFloat`, `endRadiusFraction: CGFloat` | |:white_check_mark:| EmptyView | | |:white_check_mark:| EquatableView | `contained view` | |:white_check_mark:| Font (*) | `size: CGFloat`, `isFixedSize: Bool`, `name: String`, `weight: Font.Weight`, `design: Font.Design`, `style: Font.TextStyle` | From c766db3be0eff8994bcdc92718dfc7fc19baf3ca Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Sep 2021 12:49:51 +0300 Subject: [PATCH 04/54] Fix the link to the code snippet --- guide_watchOS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guide_watchOS.md b/guide_watchOS.md index 6d1b9007..9381747e 100644 --- a/guide_watchOS.md +++ b/guide_watchOS.md @@ -1,6 +1,6 @@ # View Hosting on watchOS -Because of WatchKit API limitations **ViewInspector** currently cannot automatically host your views for asynchronous tests - you need to add [this swift file](https://github.com/nalexn/ViewInspector/blob/0.8.2/.watchOS/watchOS-Ext/watchOSApp%2BTestable.swift) to your **watchOS extension target**. +Because of WatchKit API limitations **ViewInspector** currently cannot automatically host your views for asynchronous tests - you need to add [this swift file](https://github.com/nalexn/ViewInspector/blob/master/.watchOS/watchOS-Ext/watchOSApp%2BTestable.swift) to your **watchOS extension target**. Then, add the appropriate code snippet, depending on your setup: @@ -75,4 +75,4 @@ extension View { self } } -``` \ No newline at end of file +``` From a9a97be14af7d61a68cc6d6f34ab3db3174a9b4a Mon Sep 17 00:00:00 2001 From: Joe Newton Date: Wed, 22 Sep 2021 11:40:48 -0400 Subject: [PATCH 05/54] Added method for inspecting `ListItemTint` values --- Sources/ViewInspector/SwiftUI/List.swift | 11 +++++++ .../SwiftUI/ListTests.swift | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/Sources/ViewInspector/SwiftUI/List.swift b/Sources/ViewInspector/SwiftUI/List.swift index 041dd93f..9ed6b4e9 100644 --- a/Sources/ViewInspector/SwiftUI/List.swift +++ b/Sources/ViewInspector/SwiftUI/List.swift @@ -54,6 +54,17 @@ public extension InspectableView { return try contentForModifierLookup.listRowBackground(parent: self, index: index) } + func listItemTint() throws -> (color: Color, isFixed: Bool) { + let color = try modifierAttribute( + modifierName: "_TraitWritingModifier", + path: "modifier|value|some|effect|color", type: Color.self, call: "listItemTint") + let isFixed = try modifierAttribute( + modifierName: "_TraitWritingModifier", + path: "modifier|value|some|isFixed", type: Bool.self, call: "listItemTint") + + return (color, isFixed) + } + func listStyle() throws -> Any { let modifier = try self.modifier({ modifier -> Bool in return modifier.modifierType.hasPrefix("ListStyleWriter") diff --git a/Tests/ViewInspectorTests/SwiftUI/ListTests.swift b/Tests/ViewInspectorTests/SwiftUI/ListTests.swift index f57ae390..01f6afd9 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ListTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ListTests.swift @@ -101,6 +101,36 @@ final class GlobalModifiersForList: XCTestCase { let sut = EmptyView().listRowBackground(Text("test").padding()) XCTAssertNoThrow(try sut.inspect().find(text: "test")) } + + func testListItemTint() throws { + guard #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) else { return } + let sut = EmptyView().listItemTint(.red) + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testFixedListItemTint() throws { + guard #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) else { return } + let sut = EmptyView().listItemTint(.fixed(.red)) + let tint = try sut.inspect().listItemTint() + XCTAssertEqual(tint.color, .red) + XCTAssertTrue(tint.isFixed) + } + + func testListItemTintColor() throws { + guard #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) else { return } + let sut = EmptyView().listItemTint(.red) + let tint = try sut.inspect().listItemTint() + XCTAssertEqual(tint.color, .red) + XCTAssertTrue(tint.isFixed) + } + + func testPreferredListItemTint() throws { + guard #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) else { return } + let sut = EmptyView().listItemTint(.preferred(.red)) + let tint = try sut.inspect().listItemTint() + XCTAssertEqual(tint.color, .red) + XCTAssertFalse(tint.isFixed) + } func testListStyle() throws { let sut = EmptyView().listStyle(DefaultListStyle()) From a1c2b6a0d4b62a8c22430fea1343259c9947e4fd Mon Sep 17 00:00:00 2001 From: Joe Newton Date: Thu, 23 Sep 2021 09:27:48 -0400 Subject: [PATCH 06/54] Added method for inspecting `tint()` values --- .../ViewInspector/Modifiers/PreviewModifiers.swift | 7 +++++++ .../ViewModifiers/PresentationModifiersTests.swift | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Sources/ViewInspector/Modifiers/PreviewModifiers.swift b/Sources/ViewInspector/Modifiers/PreviewModifiers.swift index 122e1567..aab42a6d 100644 --- a/Sources/ViewInspector/Modifiers/PreviewModifiers.swift +++ b/Sources/ViewInspector/Modifiers/PreviewModifiers.swift @@ -33,6 +33,13 @@ public extension InspectableView { let keyPath = try Inspector.environmentKeyPath(Optional.self, reference) return try environment(keyPath, call: "accentColor") } + + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func tint() throws -> Color? { + let reference = EmptyView().tint(nil) + let keyPath = try Inspector.environmentKeyPath(Optional.self, reference) + return try environment(keyPath, call: "tint") + } func colorScheme() throws -> ColorScheme { let reference = EmptyView().colorScheme(.light) diff --git a/Tests/ViewInspectorTests/ViewModifiers/PresentationModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/PresentationModifiersTests.swift index 1fbc30b0..522be5cb 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/PresentationModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/PresentationModifiersTests.swift @@ -44,6 +44,19 @@ final class ViewColorTests: XCTestCase { XCTAssertEqual(try view.foregroundColor(), .red) } #endif + + func testTint() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } + let sut = EmptyView().tint(.green) + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testTintInspection() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } + let sut = Group { EmptyView() }.tint(.green) + XCTAssertEqual(try sut.inspect().group().tint(), .green) + XCTAssertEqual(try sut.inspect().group().emptyView(0).tint(), .green) + } func testColorScheme() throws { let sut = EmptyView().colorScheme(.light) From c8e5b2f641d98795a3a4219d3ef2281ccffb3d76 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Sun, 14 Nov 2021 16:47:04 +0300 Subject: [PATCH 07/54] Add tint and listItemTint to readiness --- readiness.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readiness.md b/readiness.md index 0aa839c7..a04ed5f0 100644 --- a/readiness.md +++ b/readiness.md @@ -438,6 +438,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:white_check_mark:| `func foregroundColor(Color?) -> some View` | |:white_check_mark:| `func accentColor(Color?) -> some View` | +|:white_check_mark:| `func tint(Color?) -> some View` | +|:white_check_mark:| `func listItemTint(Color?) -> some View` | ### Adopting View Color Schemes From 53bc328b3c7594010ab778502725483d875e7e90 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Sun, 21 Nov 2021 14:48:38 +0300 Subject: [PATCH 08/54] #145 Add ENABLE_TESTING_SEARCH_PATHS=YES to the project settings --- ViewInspector.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ViewInspector.xcodeproj/project.pbxproj b/ViewInspector.xcodeproj/project.pbxproj index f46b094b..cb72f07b 100644 --- a/ViewInspector.xcodeproj/project.pbxproj +++ b/ViewInspector.xcodeproj/project.pbxproj @@ -1313,6 +1313,7 @@ ENABLE_NS_ASSERTIONS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -1421,6 +1422,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( From fd3b1c48ee5cb44e96a9fe3027f8cc78b5a4e274 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Sun, 21 Nov 2021 23:18:21 +0300 Subject: [PATCH 09/54] Fix #129 and #136: Introduce AbsentView and isAbsent flag --- ...spectableView+RandomAccessCollection.swift | 47 +++++++++++++++---- Sources/ViewInspector/LazyGroup.swift | 44 ----------------- .../InspectableViewTests.swift | 32 +++++++++++++ Tests/ViewInspectorTests/LazyGroupTests.swift | 31 ++++++------ 4 files changed, 85 insertions(+), 69 deletions(-) diff --git a/Sources/ViewInspector/InspectableView+RandomAccessCollection.swift b/Sources/ViewInspector/InspectableView+RandomAccessCollection.swift index e9323f67..bf769efd 100644 --- a/Sources/ViewInspector/InspectableView+RandomAccessCollection.swift +++ b/Sources/ViewInspector/InspectableView+RandomAccessCollection.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension InspectableView: Sequence where View: MultipleViewContent { @@ -7,17 +8,19 @@ extension InspectableView: Sequence where View: MultipleViewContent { public struct Iterator: IteratorProtocol { - private var groupIterator: LazyGroup.Iterator + private var index: Int = -1 + private let group: LazyGroup private let view: UnwrappedView init(_ group: LazyGroup, view: UnwrappedView) { - groupIterator = group.makeIterator() + self.group = group self.view = view } mutating public func next() -> Element? { - guard let content = groupIterator.next() - else { return nil } + index += 1 + guard index < group.count else { return nil } + let content = (try? group.element(at: index)) ?? .absentView return try? .init(content, parent: view) } } @@ -45,11 +48,39 @@ extension InspectableView: Collection, BidirectionalCollection, RandomAccessColl public var count: Int { underestimatedCount } public subscript(index: Index) -> Iterator.Element { - // swiftlint:disable force_try - let viewes = try! View.children(content) - return try! .init(try! viewes.element(at: index), parent: self, call: "[\(index)]") - // swiftlint:enable force_try + do { + do { + let viewes = try View.children(content) + return try .init(try viewes.element(at: index), parent: self, call: "[\(index)]") + } catch InspectionError.viewNotFound { + return try Element(.absentView, parent: self, index: index) + } catch { throw error } + } catch { + fatalError(error.localizedDescription) + } } public func index(after index: Index) -> Index { index + 1 } } + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension Content { + + static var absentView: Content { + return Content(AbsentView()) + } + + var isAbsent: Bool { view is AbsentView } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal struct AbsentView: View { + var body: Never { fatalError() } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension InspectableView { + public var isAbsent: Bool { + return content.isAbsent + } +} diff --git a/Sources/ViewInspector/LazyGroup.swift b/Sources/ViewInspector/LazyGroup.swift index f96dd23b..f09f715f 100644 --- a/Sources/ViewInspector/LazyGroup.swift +++ b/Sources/ViewInspector/LazyGroup.swift @@ -29,47 +29,3 @@ public struct LazyGroup { } } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension LazyGroup: Sequence { - - public struct Iterator: IteratorProtocol { - public typealias Element = T - internal var index = -1 - private var group: LazyGroup - - init(group: LazyGroup) { - self.group = group - } - - mutating public func next() -> Element? { - index += 1 - do { - return try group.element(at: index) - } catch _ { - return nil - } - } - } - - public func makeIterator() -> Iterator { - .init(group: self) - } - - public var underestimatedCount: Int { count } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension LazyGroup: RandomAccessCollection { - - public var startIndex: Int { 0 } - public var endIndex: Int { count } - - public subscript(position: Int) -> T { - // swiftlint:disable force_try - return try! element(at: position) - // swiftlint:enable force_try - } - - public func index(after index: Int) -> Int { index + 1 } -} diff --git a/Tests/ViewInspectorTests/InspectableViewTests.swift b/Tests/ViewInspectorTests/InspectableViewTests.swift index ccb80eee..4656113b 100644 --- a/Tests/ViewInspectorTests/InspectableViewTests.swift +++ b/Tests/ViewInspectorTests/InspectableViewTests.swift @@ -55,4 +55,36 @@ final class InspectableViewTestsAccessTests: XCTestCase { XCTAssertNil(iterator.next()) XCTAssertEqual(sut.underestimatedCount, 3) } + + func testCollectionWithAbsentViews() throws { + let sut = try ViewWithAbsentChildren(present: false).inspect() + var counter = 0 + // `forEach` is using iterator + sut.forEach { _ in counter += 1 } + XCTAssertEqual(counter, 4) + // `map` is using subscript + let array = sut.map { try? $0.text().string() } + XCTAssertEqual(array, [nil, "b", "c", nil]) + XCTAssertTrue(sut[0].isAbsent) + XCTAssertFalse(sut[1].isAbsent) + XCTAssertFalse(sut[2].isAbsent) + XCTAssertTrue(sut[3].isAbsent) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct ViewWithAbsentChildren: View, Inspectable { + let present: Bool + + @ViewBuilder + var body: some View { + if present { + Text("a") + } + Text("b") + Text("c") + if present { + Text("d") + } + } } diff --git a/Tests/ViewInspectorTests/LazyGroupTests.swift b/Tests/ViewInspectorTests/LazyGroupTests.swift index 3ae24b73..2158a7c6 100644 --- a/Tests/ViewInspectorTests/LazyGroupTests.swift +++ b/Tests/ViewInspectorTests/LazyGroupTests.swift @@ -4,35 +4,32 @@ import XCTest @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) final class LazyGroupTests: XCTestCase { - func testLazyGroupEmpty() { + func testEmpty() { let sut = LazyGroup.empty XCTAssertEqual(sut.count, 0) - XCTAssertEqual(sut.underestimatedCount, 0) } - func testLazyGroupSequence() { + func testElementAtIndex() { let count = 3 let sut = LazyGroup(count: count) { $0 } - XCTAssertEqual(sut.map { $0 }, [0, 1, 2]) - var iterator = sut.makeIterator() - for _ in 0 ..< count { - XCTAssertNotNil(iterator.next()) + for index in 0..(count: 1) { $0 } - var iterator = sut.makeIterator() - XCTAssertNotNil(iterator.next()) - XCTAssertNil(iterator.next()) - } - - func testLazyGroupPlusOperator() throws { + func testPlusOperator() throws { let group1 = LazyGroup(count: 2) { $0 } let group2 = LazyGroup(count: 3) { $0 } let sut = group1 + group2 XCTAssertEqual(sut.count, group1.count + group2.count) - XCTAssertEqual(sut.map { $0 }, [0, 1, 0, 1, 2]) + XCTAssertEqual(try sut.element(at: 0), 0) + XCTAssertEqual(try sut.element(at: 1), 1) + XCTAssertEqual(try sut.element(at: 2), 0) + XCTAssertEqual(try sut.element(at: 3), 1) + XCTAssertEqual(try sut.element(at: 4), 2) } } From aa56c1db09d50bebbc9daaa9b3f23e1c02faa72a Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Thu, 25 Nov 2021 17:33:15 +0300 Subject: [PATCH 10/54] Make AbsentView report isResponsive() false --- Sources/ViewInspector/InspectableView.swift | 3 +++ Tests/ViewInspectorTests/InspectableViewTests.swift | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Sources/ViewInspector/InspectableView.swift b/Sources/ViewInspector/InspectableView.swift index 0a064eac..62fc23d8 100644 --- a/Sources/ViewInspector/InspectableView.swift +++ b/Sources/ViewInspector/InspectableView.swift @@ -355,6 +355,9 @@ public extension InspectableView { throw InspectionError.unresponsiveControl( name: name, reason: blocker.pathToRoot.ifEmpty(use: "it") + " has allowsHitTesting set to false") } + if isAbsent { + throw InspectionError.unresponsiveControl(name: name, reason: "the conditional view has been hidden") + } } private func farthestParent(where condition: (InspectableView) -> Bool) -> UnwrappedView? { diff --git a/Tests/ViewInspectorTests/InspectableViewTests.swift b/Tests/ViewInspectorTests/InspectableViewTests.swift index 4656113b..2afaac8f 100644 --- a/Tests/ViewInspectorTests/InspectableViewTests.swift +++ b/Tests/ViewInspectorTests/InspectableViewTests.swift @@ -69,6 +69,8 @@ final class InspectableViewTestsAccessTests: XCTestCase { XCTAssertFalse(sut[1].isAbsent) XCTAssertFalse(sut[2].isAbsent) XCTAssertTrue(sut[3].isAbsent) + XCTAssertTrue(sut[2].isResponsive()) + XCTAssertFalse(sut[3].isResponsive()) } } From d0bda93cf8fa1ced73e7b17fb8326b81c758b2b5 Mon Sep 17 00:00:00 2001 From: Per-Olof Bengtsson Date: Mon, 13 Dec 2021 15:02:19 +0100 Subject: [PATCH 11/54] fix so that environmentObjects are injected into .actualView(), nalexn/ViewInspector#149 --- Sources/ViewInspector/SwiftUI/CustomView.swift | 6 +++++- Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/ViewInspector/SwiftUI/CustomView.swift b/Sources/ViewInspector/SwiftUI/CustomView.swift index 6071a8c0..4b7bde43 100644 --- a/Sources/ViewInspector/SwiftUI/CustomView.swift +++ b/Sources/ViewInspector/SwiftUI/CustomView.swift @@ -101,7 +101,11 @@ public extension InspectableView where View: MultipleViewContent { public extension InspectableView where View: CustomViewType { func actualView() throws -> View.T { - return try Inspector.cast(value: content.view, type: View.T.self) + var view = try Inspector.cast(value: content.view, type: View.T.self) + content.medium.environmentObjects.forEach { + view.inject(environmentObject: $0) + } + return view } } diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift index 9d4172e4..a57d8f80 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift @@ -49,6 +49,12 @@ final class CustomViewTests: XCTestCase { let view = EnvironmentStateTestView().environmentObject(viewModel) XCTAssertNoThrow(try view.inspect().view(EnvironmentStateTestView.self)) } + + func testEnvironmentObjectActualView() throws { + let viewModel = ExternalState() + let view = EnvironmentStateTestView().environmentObject(viewModel) + _ = try view.inspect().view(EnvironmentStateTestView.self).actualView().viewModel + } func testResetsModifiers() throws { let view = SimpleTestView().padding() From d1d01c197bee78983e9b5d3fed0e3c7b064d76b2 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Mon, 20 Dec 2021 18:49:36 +0300 Subject: [PATCH 12/54] fix #138: Rework view type matching --- Sources/ViewInspector/InspectableView.swift | 20 ++--- Sources/ViewInspector/Inspector.swift | 90 +++++++++++-------- Sources/ViewInspector/PopupPresenter.swift | 8 +- .../ViewInspector/SwiftUI/CustomView.swift | 10 ++- Sources/ViewInspector/SwiftUI/Gesture.swift | 4 +- Sources/ViewInspector/SwiftUI/Shape.swift | 2 +- .../SwiftUI/StyleConfiguration.swift | 10 +-- Sources/ViewInspector/ViewSearch.swift | 2 +- Sources/ViewInspector/ViewSearchIndex.swift | 13 +-- Tests/ViewInspectorTests/InspectorTests.swift | 2 +- .../SwiftUI/CustomViewBuilderTests.swift | 7 +- .../SwiftUI/CustomViewTests.swift | 46 ++++++++++ 12 files changed, 144 insertions(+), 70 deletions(-) diff --git a/Sources/ViewInspector/InspectableView.swift b/Sources/ViewInspector/InspectableView.swift index 62fc23d8..b72ac95d 100644 --- a/Sources/ViewInspector/InspectableView.swift +++ b/Sources/ViewInspector/InspectableView.swift @@ -36,10 +36,10 @@ public struct InspectableView where View: KnownViewType { inspectionCall: inspectionCall) } catch { if let err = error as? InspectionError, case .typeMismatch = err { - let factual = Inspector.typeName(value: content.view, namespaced: true, prefixOnly: true) - .sanitizeNamespace() + let factual = Inspector.typeName(value: content.view, namespaced: true) + .removingSwiftUINamespace() let expected = View.namespacedPrefixes - .map { $0.sanitizeNamespace() } + .map { $0.removingSwiftUINamespace() } .joined(separator: " or ") throw InspectionError.inspection(path: pathToRoot, factual: factual, expected: expected) } @@ -49,13 +49,9 @@ public struct InspectableView where View: KnownViewType { } private extension String { - func sanitizeNamespace() -> String { - var str = self - if let range = str.range(of: ".(unknown context at ") { - let end = str.index(range.upperBound, offsetBy: .init(11)) - str.replaceSubrange(range.lowerBound.. String { + guard hasPrefix("SwiftUI.") else { return self } + return String(suffix(count - 8)) } } @@ -309,7 +305,7 @@ extension ModifierNameProvider { extension ModifiedContent: ModifierNameProvider { func modifierType(prefixOnly: Bool) -> String { - return Inspector.typeName(type: Modifier.self, prefixOnly: prefixOnly) + return Inspector.typeName(type: Modifier.self, replacingGenerics: prefixOnly ? "" : nil) } var customModifier: Inspectable? { @@ -339,7 +335,7 @@ public extension InspectableView { } internal func guardIsResponsive() throws { - let name = Inspector.typeName(value: content.view, prefixOnly: true) + let name = Inspector.typeName(value: content.view, replacingGenerics: "") if isDisabled() { let blocker = farthestParent(where: { $0.isDisabled() }) ?? self throw InspectionError.unresponsiveControl( diff --git a/Sources/ViewInspector/Inspector.swift b/Sources/ViewInspector/Inspector.swift index 4da98e60..9446827f 100644 --- a/Sources/ViewInspector/Inspector.swift +++ b/Sources/ViewInspector/Inspector.swift @@ -43,28 +43,48 @@ internal extension Inspector { static func typeName(value: Any, namespaced: Bool = false, - prefixOnly: Bool = false) -> String { - return typeName(type: type(of: value), namespaced: namespaced, prefixOnly: prefixOnly) + replacingGenerics: String? = nil) -> String { + return typeName(type: type(of: value), namespaced: namespaced, + replacingGenerics: replacingGenerics) } static func typeName(type: Any.Type, namespaced: Bool = false, - prefixOnly: Bool = false) -> String { - let typeName = namespaced ? String(reflecting: type) : String(describing: type) - guard prefixOnly else { return typeName } - let name = typeName.components(separatedBy: "<").first! - guard namespaced else { return name } - let string = NSMutableString(string: name) - let range = NSRange(location: 0, length: string.length) - namespaceSanitizeRegex.replaceMatches(in: string, options: [], range: range, withTemplate: "SwiftUI") - return String(string) - } - - private static var namespaceSanitizeRegex: NSRegularExpression = { - guard let regex = try? NSRegularExpression(pattern: "SwiftUI.\\(unknown context at .*\\)", options: []) - else { fatalError() } - return regex - }() + replacingGenerics: String? = nil) -> String { + let typeName = namespaced ? String(reflecting: type).sanitizingNamespace() : String(describing: type) + return replacingGenerics.flatMap { + typeName.replacingGenericParameters($0) + } ?? typeName + } +} + +private extension String { + func sanitizingNamespace() -> String { + var str = self + if let range = str.range(of: ".(unknown context at ") { + let end = str.index(range.upperBound, offsetBy: .init(11)) + str.replaceSubrange(range.lowerBound.. String { + guard let start = self.firstIndex(of: "<") + else { return self } + var balance = 1 + var current = self.index(after: start) + while balance > 0 && current < endIndex { + let char = self[current] + if char == "<" { balance += 1 } + if char == ">" { balance -= 1 } + current = self.index(after: current) + } + if balance == 0 { + return String(self[.. Bool { - return Inspector.typeName(value: view, prefixOnly: true) == ViewType.TupleView.typePrefix + return Inspector.typeName(value: view, replacingGenerics: "") == ViewType.TupleView.typePrefix } static func unwrap(view: Any, medium: Content.Medium) throws -> Content { @@ -189,7 +209,7 @@ internal extension Inspector { // swiftlint:disable cyclomatic_complexity static func unwrap(content: Content) throws -> Content { - switch Inspector.typeName(value: content.view, prefixOnly: true) { + switch Inspector.typeName(value: content.view, replacingGenerics: "") { case "Tree": return try ViewType.TreeView.child(content) case "IDView": @@ -218,23 +238,23 @@ internal extension Inspector { static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws { - for prefix in namespacedPrefixes { - let withGenericParams = prefix.contains("<") - let typePrefix = typeName(type: type(of: value), namespaced: true, prefixOnly: !withGenericParams) - if typePrefix == "SwiftUI.EnvironmentReaderView" { - let typeWithParams = typeName(type: type(of: value)) - if typeWithParams.contains("NavigationBarItemsKey") { - throw InspectionError.notSupported( - """ - Please insert '.navigationBarItems()' before \(inspectionCall) \ - for unwrapping the underlying view hierarchy. - """) - } - } - if namespacedPrefixes.contains(typePrefix) { - return + var typePrefix = typeName(type: type(of: value), namespaced: true, replacingGenerics: "") + if typePrefix == ViewType.popupContainerTypePrefix { + typePrefix = typeName(type: type(of: value), namespaced: true) + } + if typePrefix == "SwiftUI.EnvironmentReaderView" { + let typeWithParams = typeName(type: type(of: value)) + if typeWithParams.contains("NavigationBarItemsKey") { + throw InspectionError.notSupported( + """ + Please insert '.navigationBarItems()' before \(inspectionCall) \ + for unwrapping the underlying view hierarchy. + """) } } + if namespacedPrefixes.contains(typePrefix) { + return + } if let prefix = namespacedPrefixes.first { let typePrefix = typeName(type: type(of: value), namespaced: true) throw InspectionError.typeMismatch(factual: typePrefix, expected: prefix) diff --git a/Sources/ViewInspector/PopupPresenter.swift b/Sources/ViewInspector/PopupPresenter.swift index c702457e..dda57022 100644 --- a/Sources/ViewInspector/PopupPresenter.swift +++ b/Sources/ViewInspector/PopupPresenter.swift @@ -146,10 +146,16 @@ internal extension ViewType { } } +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal extension ViewType { + static var popupContainerTypePrefix = "ViewInspector.ViewType.PopupContainer" +} + @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewType.PopupContainer { static var typePrefix: String { - "ViewInspector.ViewType.PopupContainer" + return ViewType.popupContainerTypePrefix + + "" } } diff --git a/Sources/ViewInspector/SwiftUI/CustomView.swift b/Sources/ViewInspector/SwiftUI/CustomView.swift index 6071a8c0..1fea77c7 100644 --- a/Sources/ViewInspector/SwiftUI/CustomView.swift +++ b/Sources/ViewInspector/SwiftUI/CustomView.swift @@ -8,17 +8,19 @@ public protocol CustomViewType { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension ViewType { + internal static let customViewGenericsPlaceholder = "" + struct View: KnownViewType, CustomViewType where T: Inspectable { public static var typePrefix: String { guard T.self != ViewType.Stub.self else { return "" } - return Inspector.typeName(type: T.self, prefixOnly: true) + return Inspector.typeName(type: T.self, replacingGenerics: customViewGenericsPlaceholder) } public static var namespacedPrefixes: [String] { guard T.self != ViewType.Stub.self else { return [] } - return [Inspector.typeName(type: T.self, namespaced: true, prefixOnly: true)] + return [Inspector.typeName(type: T.self, namespaced: true, replacingGenerics: "")] } public static func inspectionCall(typeName: String) -> String { @@ -72,7 +74,7 @@ public extension InspectableView where View: SingleViewContent { func view(_ type: T.Type) throws -> InspectableView> where T: Inspectable { let child = try View.child(content) - let prefix = Inspector.typeName(type: type, namespaced: true, prefixOnly: true) + let prefix = Inspector.typeName(type: type, namespaced: true, replacingGenerics: "") let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) let call = ViewType.inspectionCall(base: base, index: nil) try Inspector.guardType(value: child.view, namespacedPrefixes: [prefix], inspectionCall: call) @@ -87,7 +89,7 @@ public extension InspectableView where View: MultipleViewContent { func view(_ type: T.Type, _ index: Int) throws -> InspectableView> where T: Inspectable { let content = try child(at: index) - let prefix = Inspector.typeName(type: type, namespaced: true, prefixOnly: true) + let prefix = Inspector.typeName(type: type, namespaced: true, replacingGenerics: "") let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) let call = ViewType.inspectionCall(base: base, index: index) try Inspector.guardType(value: content.view, namespacedPrefixes: [prefix], inspectionCall: call) diff --git a/Sources/ViewInspector/SwiftUI/Gesture.swift b/Sources/ViewInspector/SwiftUI/Gesture.swift index 18c0f1c7..512937a0 100644 --- a/Sources/ViewInspector/SwiftUI/Gesture.swift +++ b/Sources/ViewInspector/SwiftUI/Gesture.swift @@ -11,7 +11,7 @@ public extension ViewType { struct Gesture: KnownViewType, GestureViewType where T: SwiftUI.Gesture & Inspectable { public static var typePrefix: String { - return Inspector.typeName(type: T.self, prefixOnly: true) + return Inspector.typeName(type: T.self, replacingGenerics: "") } public static var namespacedPrefixes: [String] { @@ -25,7 +25,7 @@ public extension ViewType { "SwiftUI._ModifiersGesture", "SwiftUI.GestureStateGesture" ] - prefixes.append(Inspector.typeName(type: T.self, namespaced: true, prefixOnly: true)) + prefixes.append(Inspector.typeName(type: T.self, namespaced: true, replacingGenerics: "")) return prefixes } diff --git a/Sources/ViewInspector/SwiftUI/Shape.swift b/Sources/ViewInspector/SwiftUI/Shape.swift index 7fdd22a7..f76489b1 100644 --- a/Sources/ViewInspector/SwiftUI/Shape.swift +++ b/Sources/ViewInspector/SwiftUI/Shape.swift @@ -107,7 +107,7 @@ private extension InspectableView { } func lookupShape(_ view: Any, typeName: String, label: String) throws -> Any { - let name = Inspector.typeName(value: view, prefixOnly: true) + let name = Inspector.typeName(value: view, replacingGenerics: "") if name.hasPrefix(typeName) { return view } diff --git a/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift b/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift index 96e84ad3..63694680 100644 --- a/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift +++ b/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift @@ -24,7 +24,7 @@ public extension ViewType.StyleConfiguration { #endif } return types - .map { Inspector.typeName(type: $0, namespaced: true, prefixOnly: true) } + .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } } public static func inspectionCall(typeName: String) -> String { @@ -44,7 +44,7 @@ public extension ViewType.StyleConfiguration { #endif } return types - .map { Inspector.typeName(type: $0, namespaced: true, prefixOnly: true) } + .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } } public static func inspectionCall(typeName: String) -> String { @@ -61,7 +61,7 @@ public extension ViewType.StyleConfiguration { types.append(LabelStyleConfiguration.Title.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, prefixOnly: true) } + .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } } public static func inspectionCall(typeName: String) -> String { @@ -78,7 +78,7 @@ public extension ViewType.StyleConfiguration { types.append(LabelStyleConfiguration.Icon.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, prefixOnly: true) } + .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } } public static func inspectionCall(typeName: String) -> String { @@ -95,7 +95,7 @@ public extension ViewType.StyleConfiguration { types.append(ProgressViewStyleConfiguration.CurrentValueLabel.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, prefixOnly: true) } + .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } } public static func inspectionCall(typeName: String) -> String { diff --git a/Sources/ViewInspector/ViewSearch.swift b/Sources/ViewInspector/ViewSearch.swift index 0f1b88b4..37f26f10 100644 --- a/Sources/ViewInspector/ViewSearch.swift +++ b/Sources/ViewInspector/ViewSearch.swift @@ -343,7 +343,7 @@ private extension UnwrappedView { func blockersDescription(_ views: [Any]) -> [String] { return views.map { view -> String in - let name = Inspector.typeName(value: view, prefixOnly: false) + let name = Inspector.typeName(value: view) if name.hasPrefix("EnvironmentReaderView") { return "navigationBarItems" } diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index ae3ed75f..19643755 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -77,16 +77,17 @@ internal extension ViewSearch { if content.isShape { return .init(ViewType.Shape.self) } - let shortPrefix = Inspector.typeName(value: content.view, prefixOnly: true) - let longPrefix = Inspector.typeName(value: content.view, namespaced: true, prefixOnly: true) - if shortPrefix.count > 0, - let identity = index[String(shortPrefix.prefix(1))]? - .first(where: { $0.viewType.namespacedPrefixes.contains(longPrefix) }) { + let shortName = Inspector.typeName(value: content.view, replacingGenerics: "") + let fullName = Inspector.typeName(value: content.view, namespaced: true, replacingGenerics: "") + if shortName.count > 0, + let identity = index[String(shortName.prefix(1))]? + .first(where: { $0.viewType.namespacedPrefixes.contains(fullName) }) { return identity } if (try? content.extractCustomView()) != nil, let inspectable = content.view as? Inspectable { - let name = Inspector.typeName(value: content.view, prefixOnly: true) + let name = Inspector.typeName( + value: content.view, replacingGenerics: ViewType.customViewGenericsPlaceholder) switch inspectable.entity { case .view: return .init(ViewType.View.self, genericTypeName: name) diff --git a/Tests/ViewInspectorTests/InspectorTests.swift b/Tests/ViewInspectorTests/InspectorTests.swift index 7b277796..cccf2f10 100644 --- a/Tests/ViewInspectorTests/InspectorTests.swift +++ b/Tests/ViewInspectorTests/InspectorTests.swift @@ -44,7 +44,7 @@ final class InspectorTests: XCTestCase { XCTAssertEqual(name1, "Struct3") let name2 = Inspector.typeName(value: testValue) XCTAssertEqual(name2, "Struct1") - let name3 = Inspector.typeName(value: Struct3(), prefixOnly: true) + let name3 = Inspector.typeName(value: Struct3(), replacingGenerics: "") XCTAssertEqual(name3, "Struct3") } diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift index 3859d3df..1325fd27 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift @@ -82,8 +82,11 @@ final class CustomViewBuilderTests: XCTestCase { let view = HStack { TestViewBuilderView { Text("Test"); EmptyView() } } - let sut = try view.inspect().find(text: "Test").pathToRoot - XCTAssertEqual(sut, "hStack().view(TestViewBuilderView.self, 0).text(0)") + let sut = try view.inspect() + let path1 = try sut.find(text: "Test").pathToRoot + let path2 = try sut.hStack().view(TestViewBuilderView.self, 0).text(0).pathToRoot + XCTAssertEqual(path1, "hStack().view(TestViewBuilderView.self, 0).text(0)") + XCTAssertEqual(path2, "hStack().view(TestViewBuilderView.self, 0).text(0)") } } diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift index 9d4172e4..b7b595f7 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift @@ -192,6 +192,22 @@ final class CustomViewTests: XCTestCase { XCTAssertEqual(sut2, "hStack().view(SimpleTestView.self, 0)") } + func testViewsWithSameNamePrefix() throws { + let sut = try AnyView(NameMatchViewList()).inspect() + XCTAssertEqual(try sut.find(NameMatchViewList.self).pathToRoot, + "anyView().view(NameMatchViewList.self)") + XCTAssertEqual(try sut.find(NameMatchView.self).pathToRoot, + "anyView().view(NameMatchViewList.self).forEach().view(NameMatchView.self, 0)") + } + + func testViewContainerWithGenericParameter() throws { + let sut = try AnyView(GenericContainer()).inspect() + XCTAssertEqual(try sut.find(GenericContainer.self).pathToRoot, + "anyView().view(GenericContainer.self)") + XCTAssertEqual(try sut.find(GenericContainer.TestView.self).pathToRoot, + "anyView().view(GenericContainer.self).view(TestView.self)") + } + func testTestViews() { XCTAssertNoThrow(NonInspectableTestView().body) XCTAssertNoThrow(SimpleTestView().body) @@ -353,3 +369,33 @@ extension ViewType { static var namespacedPrefixes: [String] { ["Swift.String"] } } } + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct NameMatchView: View, Inspectable { + var body: some View { + Text("") + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct NameMatchViewList: View, Inspectable { + var body: some View { + ForEach(0..<5) { _ in NameMatchView() } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct GenericContainer: View, Inspectable { + var body: some View { + TestView() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private extension GenericContainer { + struct TestView: View, Inspectable { + var body: some View { + Text("") + } + } +} From 8f36ba170dd1d4fcac54ab548266c59960baf9af Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Mon, 20 Dec 2021 19:57:58 +0300 Subject: [PATCH 13/54] Add a test when incorrect generic parameter does not allow the view cast --- Sources/ViewInspector/Inspector.swift | 10 +++++++--- Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/ViewInspector/Inspector.swift b/Sources/ViewInspector/Inspector.swift index 9446827f..1ef2efa3 100644 --- a/Sources/ViewInspector/Inspector.swift +++ b/Sources/ViewInspector/Inspector.swift @@ -265,8 +265,12 @@ internal extension Inspector { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension InspectionError { static func typeMismatch(_ value: V, _ expectedType: T.Type) -> InspectionError { - return .typeMismatch( - factual: Inspector.typeName(value: value), - expected: Inspector.typeName(type: expectedType)) + var factual = Inspector.typeName(value: value) + var expected = Inspector.typeName(type: expectedType) + if factual == expected { + factual = Inspector.typeName(value: value, namespaced: true) + expected = Inspector.typeName(type: expectedType, namespaced: true) + } + return .typeMismatch(factual: factual, expected: expected) } } diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift index b7b595f7..595622b2 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift @@ -208,6 +208,16 @@ final class CustomViewTests: XCTestCase { "anyView().view(GenericContainer.self).view(TestView.self)") } + func testViewContainerGenericParameterMismatch() throws { + let sut = try AnyView(GenericContainer()).inspect() + let view = try sut.find(GenericContainer.TestView.self) + XCTAssertThrows(try view.actualView(), + """ + Type mismatch: ViewInspectorTests.GenericContainer.TestView \ + is not ViewInspectorTests.GenericContainer.TestView + """) + } + func testTestViews() { XCTAssertNoThrow(NonInspectableTestView().body) XCTAssertNoThrow(SimpleTestView().body) From ee49ae1f87009b876b7fcf2d683e900f34188cef Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 03:56:26 +0300 Subject: [PATCH 14/54] Refactor typeName function --- Sources/ViewInspector/InspectableView.swift | 4 +-- Sources/ViewInspector/Inspector.swift | 30 +++++++++++++------ .../ViewInspector/SwiftUI/CustomView.swift | 8 ++--- Sources/ViewInspector/SwiftUI/Gesture.swift | 4 +-- Sources/ViewInspector/SwiftUI/Shape.swift | 2 +- .../SwiftUI/StyleConfiguration.swift | 10 +++---- Sources/ViewInspector/ViewSearchIndex.swift | 6 ++-- Tests/ViewInspectorTests/InspectorTests.swift | 2 +- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/Sources/ViewInspector/InspectableView.swift b/Sources/ViewInspector/InspectableView.swift index b72ac95d..805b672c 100644 --- a/Sources/ViewInspector/InspectableView.swift +++ b/Sources/ViewInspector/InspectableView.swift @@ -305,7 +305,7 @@ extension ModifierNameProvider { extension ModifiedContent: ModifierNameProvider { func modifierType(prefixOnly: Bool) -> String { - return Inspector.typeName(type: Modifier.self, replacingGenerics: prefixOnly ? "" : nil) + return Inspector.typeName(type: Modifier.self, generics: prefixOnly ? .remove : .keep) } var customModifier: Inspectable? { @@ -335,7 +335,7 @@ public extension InspectableView { } internal func guardIsResponsive() throws { - let name = Inspector.typeName(value: content.view, replacingGenerics: "") + let name = Inspector.typeName(value: content.view, generics: .remove) if isDisabled() { let blocker = farthestParent(where: { $0.isDisabled() }) ?? self throw InspectionError.unresponsiveControl( diff --git a/Sources/ViewInspector/Inspector.swift b/Sources/ViewInspector/Inspector.swift index 1ef2efa3..6e8c237b 100644 --- a/Sources/ViewInspector/Inspector.swift +++ b/Sources/ViewInspector/Inspector.swift @@ -41,20 +41,32 @@ internal extension Inspector { return casted } + enum GenericParameters { + case keep + case remove + case customViewPlaceholder + } + static func typeName(value: Any, namespaced: Bool = false, - replacingGenerics: String? = nil) -> String { + generics: GenericParameters = .keep) -> String { return typeName(type: type(of: value), namespaced: namespaced, - replacingGenerics: replacingGenerics) + generics: generics) } static func typeName(type: Any.Type, namespaced: Bool = false, - replacingGenerics: String? = nil) -> String { + generics: GenericParameters = .keep) -> String { let typeName = namespaced ? String(reflecting: type).sanitizingNamespace() : String(describing: type) - return replacingGenerics.flatMap { - typeName.replacingGenericParameters($0) - } ?? typeName + switch generics { + case .keep: + return typeName + case .remove: + return typeName.replacingGenericParameters("") + case .customViewPlaceholder: + let parameters = ViewType.customViewGenericsPlaceholder + return typeName.replacingGenericParameters(parameters) + } } } @@ -200,7 +212,7 @@ internal extension Inspector { } static func isTupleView(_ view: Any) -> Bool { - return Inspector.typeName(value: view, replacingGenerics: "") == ViewType.TupleView.typePrefix + return Inspector.typeName(value: view, generics: .remove) == ViewType.TupleView.typePrefix } static func unwrap(view: Any, medium: Content.Medium) throws -> Content { @@ -209,7 +221,7 @@ internal extension Inspector { // swiftlint:disable cyclomatic_complexity static func unwrap(content: Content) throws -> Content { - switch Inspector.typeName(value: content.view, replacingGenerics: "") { + switch Inspector.typeName(value: content.view, generics: .remove) { case "Tree": return try ViewType.TreeView.child(content) case "IDView": @@ -238,7 +250,7 @@ internal extension Inspector { static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws { - var typePrefix = typeName(type: type(of: value), namespaced: true, replacingGenerics: "") + var typePrefix = typeName(type: type(of: value), namespaced: true, generics: .remove) if typePrefix == ViewType.popupContainerTypePrefix { typePrefix = typeName(type: type(of: value), namespaced: true) } diff --git a/Sources/ViewInspector/SwiftUI/CustomView.swift b/Sources/ViewInspector/SwiftUI/CustomView.swift index 961a39d2..26f115fe 100644 --- a/Sources/ViewInspector/SwiftUI/CustomView.swift +++ b/Sources/ViewInspector/SwiftUI/CustomView.swift @@ -14,13 +14,13 @@ public extension ViewType { public static var typePrefix: String { guard T.self != ViewType.Stub.self else { return "" } - return Inspector.typeName(type: T.self, replacingGenerics: customViewGenericsPlaceholder) + return Inspector.typeName(type: T.self, generics: .customViewPlaceholder) } public static var namespacedPrefixes: [String] { guard T.self != ViewType.Stub.self else { return [] } - return [Inspector.typeName(type: T.self, namespaced: true, replacingGenerics: "")] + return [Inspector.typeName(type: T.self, namespaced: true, generics: .remove)] } public static func inspectionCall(typeName: String) -> String { @@ -74,7 +74,7 @@ public extension InspectableView where View: SingleViewContent { func view(_ type: T.Type) throws -> InspectableView> where T: Inspectable { let child = try View.child(content) - let prefix = Inspector.typeName(type: type, namespaced: true, replacingGenerics: "") + let prefix = Inspector.typeName(type: type, namespaced: true, generics: .remove) let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) let call = ViewType.inspectionCall(base: base, index: nil) try Inspector.guardType(value: child.view, namespacedPrefixes: [prefix], inspectionCall: call) @@ -89,7 +89,7 @@ public extension InspectableView where View: MultipleViewContent { func view(_ type: T.Type, _ index: Int) throws -> InspectableView> where T: Inspectable { let content = try child(at: index) - let prefix = Inspector.typeName(type: type, namespaced: true, replacingGenerics: "") + let prefix = Inspector.typeName(type: type, namespaced: true, generics: .remove) let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) let call = ViewType.inspectionCall(base: base, index: index) try Inspector.guardType(value: content.view, namespacedPrefixes: [prefix], inspectionCall: call) diff --git a/Sources/ViewInspector/SwiftUI/Gesture.swift b/Sources/ViewInspector/SwiftUI/Gesture.swift index 512937a0..074b1ef1 100644 --- a/Sources/ViewInspector/SwiftUI/Gesture.swift +++ b/Sources/ViewInspector/SwiftUI/Gesture.swift @@ -11,7 +11,7 @@ public extension ViewType { struct Gesture: KnownViewType, GestureViewType where T: SwiftUI.Gesture & Inspectable { public static var typePrefix: String { - return Inspector.typeName(type: T.self, replacingGenerics: "") + return Inspector.typeName(type: T.self, generics: .remove) } public static var namespacedPrefixes: [String] { @@ -25,7 +25,7 @@ public extension ViewType { "SwiftUI._ModifiersGesture", "SwiftUI.GestureStateGesture" ] - prefixes.append(Inspector.typeName(type: T.self, namespaced: true, replacingGenerics: "")) + prefixes.append(Inspector.typeName(type: T.self, namespaced: true, generics: .remove)) return prefixes } diff --git a/Sources/ViewInspector/SwiftUI/Shape.swift b/Sources/ViewInspector/SwiftUI/Shape.swift index f76489b1..059fc330 100644 --- a/Sources/ViewInspector/SwiftUI/Shape.swift +++ b/Sources/ViewInspector/SwiftUI/Shape.swift @@ -107,7 +107,7 @@ private extension InspectableView { } func lookupShape(_ view: Any, typeName: String, label: String) throws -> Any { - let name = Inspector.typeName(value: view, replacingGenerics: "") + let name = Inspector.typeName(value: view, generics: .remove) if name.hasPrefix(typeName) { return view } diff --git a/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift b/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift index 63694680..c2d61a89 100644 --- a/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift +++ b/Sources/ViewInspector/SwiftUI/StyleConfiguration.swift @@ -24,7 +24,7 @@ public extension ViewType.StyleConfiguration { #endif } return types - .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } + .map { Inspector.typeName(type: $0, namespaced: true, generics: .remove) } } public static func inspectionCall(typeName: String) -> String { @@ -44,7 +44,7 @@ public extension ViewType.StyleConfiguration { #endif } return types - .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } + .map { Inspector.typeName(type: $0, namespaced: true, generics: .remove) } } public static func inspectionCall(typeName: String) -> String { @@ -61,7 +61,7 @@ public extension ViewType.StyleConfiguration { types.append(LabelStyleConfiguration.Title.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } + .map { Inspector.typeName(type: $0, namespaced: true, generics: .remove) } } public static func inspectionCall(typeName: String) -> String { @@ -78,7 +78,7 @@ public extension ViewType.StyleConfiguration { types.append(LabelStyleConfiguration.Icon.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } + .map { Inspector.typeName(type: $0, namespaced: true, generics: .remove) } } public static func inspectionCall(typeName: String) -> String { @@ -95,7 +95,7 @@ public extension ViewType.StyleConfiguration { types.append(ProgressViewStyleConfiguration.CurrentValueLabel.self) } return types - .map { Inspector.typeName(type: $0, namespaced: true, replacingGenerics: "") } + .map { Inspector.typeName(type: $0, namespaced: true, generics: .remove) } } public static func inspectionCall(typeName: String) -> String { diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index 19643755..b89e38d1 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -77,8 +77,8 @@ internal extension ViewSearch { if content.isShape { return .init(ViewType.Shape.self) } - let shortName = Inspector.typeName(value: content.view, replacingGenerics: "") - let fullName = Inspector.typeName(value: content.view, namespaced: true, replacingGenerics: "") + let shortName = Inspector.typeName(value: content.view, generics: .remove) + let fullName = Inspector.typeName(value: content.view, namespaced: true, generics: .remove) if shortName.count > 0, let identity = index[String(shortName.prefix(1))]? .first(where: { $0.viewType.namespacedPrefixes.contains(fullName) }) { @@ -87,7 +87,7 @@ internal extension ViewSearch { if (try? content.extractCustomView()) != nil, let inspectable = content.view as? Inspectable { let name = Inspector.typeName( - value: content.view, replacingGenerics: ViewType.customViewGenericsPlaceholder) + value: content.view, generics: .customViewPlaceholder) switch inspectable.entity { case .view: return .init(ViewType.View.self, genericTypeName: name) diff --git a/Tests/ViewInspectorTests/InspectorTests.swift b/Tests/ViewInspectorTests/InspectorTests.swift index cccf2f10..347e6fd2 100644 --- a/Tests/ViewInspectorTests/InspectorTests.swift +++ b/Tests/ViewInspectorTests/InspectorTests.swift @@ -44,7 +44,7 @@ final class InspectorTests: XCTestCase { XCTAssertEqual(name1, "Struct3") let name2 = Inspector.typeName(value: testValue) XCTAssertEqual(name2, "Struct1") - let name3 = Inspector.typeName(value: Struct3(), replacingGenerics: "") + let name3 = Inspector.typeName(value: Struct3(), generics: .remove) XCTAssertEqual(name3, "Struct3") } From 6a0ed4fe1fecc173f08c2e3f84162d31e2454fe0 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 18:15:53 +0300 Subject: [PATCH 15/54] Introduce func classified() --- Sources/ViewInspector/InspectableView.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/ViewInspector/InspectableView.swift b/Sources/ViewInspector/InspectableView.swift index 805b672c..1b52ce95 100644 --- a/Sources/ViewInspector/InspectableView.swift +++ b/Sources/ViewInspector/InspectableView.swift @@ -91,6 +91,11 @@ internal extension InspectableView { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension InspectableView { + /** + A function for accessing the parent view for introspection + - Returns: immediate predecessor of the current view in the hierarchy + - Throws: if the current view is the root + */ func parent() throws -> InspectableView { guard let parent = self.parentView else { throw InspectionError.parentViewNotFound(view: Inspector.typeName(value: content.view)) @@ -104,10 +109,22 @@ public extension InspectableView { return try .init(parent.content, parent: parent.parentView, call: parent.inspectionCall) } + /** + A property for obtaining the inspection call's chain from the root view to the current view + - Returns: A `String` representation of the inspection calls' chain + */ var pathToRoot: String { let prefix = parentView.flatMap { $0.pathToRoot } ?? "" return prefix.isEmpty ? inspectionCall : prefix + "." + inspectionCall } + + /** + A function for erasing the type of the current view + - Returns: The current view represented as a `ClassifiedView` + */ + func classified() throws -> InspectableView { + return try asInspectableView() + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) From eb74116ec13463d3cf9639b5961bcdda96b9b710 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 18:24:11 +0300 Subject: [PATCH 16/54] Enable the code requiring macOS SDK 12.0 --- Sources/ViewInspector/SwiftUI/Button.swift | 2 -- Sources/ViewInspector/SwiftUI/ConfirmationDialog.swift | 2 -- Tests/ViewInspectorTests/SwiftUI/AlertTests.swift | 2 -- Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift | 2 -- Tests/ViewInspectorTests/SwiftUI/ConfirmationDialogTests.swift | 2 -- Tests/ViewInspectorTests/SwiftUI/SafeAreaInsetTests.swift | 2 -- 6 files changed, 12 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/Button.swift b/Sources/ViewInspector/SwiftUI/Button.swift index f1e0714f..7dfad8df 100644 --- a/Sources/ViewInspector/SwiftUI/Button.swift +++ b/Sources/ViewInspector/SwiftUI/Button.swift @@ -59,13 +59,11 @@ public extension InspectableView where View == ViewType.Button { callback() } - #if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) func role() throws -> ButtonRole? { return try Inspector.attribute( label: "role", value: content.view, type: ButtonRole?.self) } - #endif } // MARK: - Global View Modifiers diff --git a/Sources/ViewInspector/SwiftUI/ConfirmationDialog.swift b/Sources/ViewInspector/SwiftUI/ConfirmationDialog.swift index 05b4c6e1..71b6e2cd 100644 --- a/Sources/ViewInspector/SwiftUI/ConfirmationDialog.swift +++ b/Sources/ViewInspector/SwiftUI/ConfirmationDialog.swift @@ -97,12 +97,10 @@ public extension InspectableView where View == ViewType.ConfirmationDialog { .asInspectableView(ofType: ViewType.ClassifiedView.self) } - #if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 func titleVisibility() throws -> Visibility { return try Inspector.attribute( label: "titleVisibility", value: content.view, type: Visibility.self) } - #endif func dismiss() throws { try isPresentedBinding().wrappedValue = false diff --git a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift index c0fad9fa..d1a1b6ce 100644 --- a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift @@ -251,7 +251,6 @@ final class DeprecatedAlertTests: XCTestCase { } } -#if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) final class AlertIOS15Tests: XCTestCase { @@ -286,7 +285,6 @@ final class AlertIOS15Tests: XCTestCase { "emptyView().alert().actions().button(1)") } } -#endif extension Int: Identifiable { public var id: Int { self } diff --git a/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift b/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift index 2bfe3097..3449b8d5 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift @@ -72,7 +72,6 @@ final class ButtonTests: XCTestCase { wait(for: [exp], timeout: 0.5) } - #if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 func testButtonRole() throws { guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { return } let sut1 = Button(role: .cancel, action: { }, label: { Text("") }) @@ -82,7 +81,6 @@ final class ButtonTests: XCTestCase { XCTAssertEqual(try sut2.inspect().button().role(), .destructive) XCTAssertNil(try sut3.inspect().button().role()) } - #endif } // MARK: - View Modifiers diff --git a/Tests/ViewInspectorTests/SwiftUI/ConfirmationDialogTests.swift b/Tests/ViewInspectorTests/SwiftUI/ConfirmationDialogTests.swift index ce47dd2a..23b2a053 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ConfirmationDialogTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ConfirmationDialogTests.swift @@ -2,7 +2,6 @@ import XCTest import SwiftUI @testable import ViewInspector -#if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) final class ConfirmationDialogTests: XCTestCase { @@ -121,4 +120,3 @@ final class ConfirmationDialogTests: XCTestCase { "group().text(1).confirmationDialog(1).actions().text(1)") } } -#endif diff --git a/Tests/ViewInspectorTests/SwiftUI/SafeAreaInsetTests.swift b/Tests/ViewInspectorTests/SwiftUI/SafeAreaInsetTests.swift index ae8117a5..3eda832f 100644 --- a/Tests/ViewInspectorTests/SwiftUI/SafeAreaInsetTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/SafeAreaInsetTests.swift @@ -2,7 +2,6 @@ import XCTest import SwiftUI @testable import ViewInspector -#if !os(macOS) && !targetEnvironment(macCatalyst) // requires macOS SDK 12.0 @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) final class SafeAreaInsetTests: XCTestCase { @@ -76,4 +75,3 @@ final class SafeAreaInsetTests: XCTestCase { "group().text(1).safeAreaInset(1).text()") } } -#endif From 9a9e8bf1e5aa2963a70a7361d84906f20c52f0f9 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 18:52:44 +0300 Subject: [PATCH 17/54] #142: Add a test illustrating the inspection difference for Alert and ActionSheet message --- .../SwiftUI/ActionSheetTests.swift | 25 +++++++++++++++++++ .../SwiftUI/AlertTests.swift | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift b/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift index 3b411cee..e0982356 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift @@ -209,6 +209,14 @@ final class ActionSheetTests: XCTestCase { XCTAssertEqual(try sut.inspect().find(text: "button_3_0").pathToRoot, "view(ActionSheetFindTestView.self).hStack().emptyView(0).actionSheet(1).button(0).labelView()") } + + func testAlertVsActionSheetMessage() throws { + let sut = try PopupMixTestView().inspect().emptyView() + let alert = try sut.alert() + let sheet = try sut.actionSheet() + XCTAssertEqual(try alert.message().text().string(), "Alert Message") + XCTAssertEqual(try sheet.message().string(), "Sheet Message") + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) @@ -279,4 +287,21 @@ private struct ActionSheetFindTestView: View, Inspectable { } } } + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct PopupMixTestView: View, Inspectable { + + @Binding var isAlertPresented = true + @Binding var isActionSheetPresented = true + + var body: some View { + EmptyView() + .alert2(isPresented: $isAlertPresented) { + Alert(title: Text("Alert"), message: Text("Alert Message"), dismissButton: nil) + } + .actionSheet2(isPresented: $isActionSheetPresented) { + ActionSheet(title: Text("Sheet"), message: Text("Sheet Message")) + } + } +} #endif diff --git a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift index d1a1b6ce..7ed900b8 100644 --- a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift @@ -295,7 +295,7 @@ extension String: Identifiable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private extension View { +extension View { func alert2(isPresented: Binding, content: @escaping () -> Alert) -> some View { return self.modifier(InspectableAlert(isPresented: isPresented, popupBuilder: content)) From 6a8e7959b2d8e59c16c019bc1301aaa3e917363b Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 22:48:27 +0300 Subject: [PATCH 18/54] Add inspection support for overlayPreferenceValue and backgroundPreferenceValue --- Sources/ViewInspector/Inspector.swift | 2 + .../SwiftUI/DelayedPreferenceView.swift | 83 +++++++++++++++++-- Sources/ViewInspector/SwiftUI/Overlay.swift | 16 ++-- .../SwiftUI/DelayedPreferenceViewTests.swift | 43 ---------- .../SwiftUI/PreferenceTests.swift | 68 +++++++++++++++ .../EnvironmentModifiersTests.swift | 60 -------------- ViewInspector.xcodeproj/project.pbxproj | 8 +- 7 files changed, 161 insertions(+), 119 deletions(-) delete mode 100644 Tests/ViewInspectorTests/SwiftUI/DelayedPreferenceViewTests.swift create mode 100644 Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift diff --git a/Sources/ViewInspector/Inspector.swift b/Sources/ViewInspector/Inspector.swift index 6e8c237b..2cfcf079 100644 --- a/Sources/ViewInspector/Inspector.swift +++ b/Sources/ViewInspector/Inspector.swift @@ -242,6 +242,8 @@ internal extension Inspector { return try ViewType.EnvironmentReaderView.child(content) case "_DelayedPreferenceView": return try ViewType.DelayedPreferenceView.child(content) + case "_PreferenceReadingView": + return try ViewType.PreferenceReadingView.child(content) default: return content } diff --git a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift index 71cf4be5..829cf0ec 100644 --- a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift +++ b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift @@ -1,20 +1,91 @@ import SwiftUI +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +public extension InspectableView { + + func overlayPreferenceValue(_ index: Int? = nil) throws -> InspectableView { + return try contentForModifierLookup + .overlay(parent: self, call: "overlayPreferenceValue", index: index) + } + + func backgroundPreferenceValue(_ index: Int? = nil) throws -> InspectableView { + return try contentForModifierLookup + .background(parent: self, call: "backgroundPreferenceValue", index: index) + } +} + @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewType { struct DelayedPreferenceView { } + struct PreferenceReadingView { } } -// MARK: - Content Extraction +// MARK: - DelayedPreferenceView @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension ViewType.DelayedPreferenceView: SingleViewContent { static func child(_ content: Content) throws -> Content { - /* Need to find a way to get through DelayedPreferenceView */ - // swiftlint:disable line_length - throw InspectionError.notSupported( - "'PreferenceValue' modifiers are currently not supported. Consider extracting the enclosed view for direct inspection.") - // swiftlint:enable line_length + let provider = try Inspector.cast(value: content.view, type: DelayedPreferenceContentProvider.self) + let view = try provider.view() + return try Inspector.unwrap(content: Content(view, medium: content.medium)) + } +} + +// MARK: - PreferenceReadingView + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension ViewType.PreferenceReadingView: SingleViewContent { + + static func child(_ content: Content) throws -> Content { + let provider = try Inspector.cast( + value: content.view, type: PreferenceReadingViewContentProvider.self) + let view = try provider.view() + let medium = content.medium.resettingViewModifiers() + return try Inspector.unwrap(content: Content(view, medium: medium)) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private protocol DelayedPreferenceContentProvider { + func view() throws -> Any +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension _DelayedPreferenceView: DelayedPreferenceContentProvider { + func view() throws -> Any { + typealias Builder = (_PreferenceValue) -> Content + let readingViewBuilder = try Inspector.attribute(label: "transform", value: self, type: Builder.self) + let prefValue = _PreferenceValue() + return readingViewBuilder(prefValue) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private extension _PreferenceValue { + struct Allocator8 { + let data: Int64 = 0 + } + + init() { + guard MemoryLayout.size == 8 else { + fatalError(MemoryLayout.actualSize()) + } + self = unsafeBitCast(Allocator8(), to: _PreferenceValue.self) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private protocol PreferenceReadingViewContentProvider { + func view() throws -> Any +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension _PreferenceReadingView: PreferenceReadingViewContentProvider { + func view() throws -> Any { + typealias Builder = (Key.Value) -> Content + let builder = try Inspector.attribute(label: "transform", value: self, type: Builder.self) + let value = Key.defaultValue + return builder(value) } } diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index bd29650a..702af0a7 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -44,31 +44,35 @@ extension ViewType.Overlay: MultipleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension Content { - func overlay(parent: UnwrappedView, index: Int?) throws -> InspectableView { + func overlay(parent: UnwrappedView, call: String = "overlay", index: Int? + ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in return modifier.modifierType.contains("_OverlayModifier") - }, call: "overlay", index: index ?? 0) + }, call: call, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) let overlayParams = ViewType.Overlay.Params(alignment: alignment) let medium = self.medium.resettingViewModifiers() .appending(viewModifier: overlayParams) let content = try Inspector.unwrap(content: Content(rootView, medium: medium)) - let call = ViewType.inspectionCall(base: "overlay(\(ViewType.indexPlaceholder))", index: index) + let base = call + "(\(ViewType.indexPlaceholder))" + let call = ViewType.inspectionCall(base: base, index: index) return try .init(content, parent: parent, call: call, index: index) } - func background(parent: UnwrappedView, index: Int?) throws -> InspectableView { + func background(parent: UnwrappedView, call: String = "background", index: Int? + ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in return modifier.modifierType.contains("_BackgroundModifier") - }, call: "background", index: index ?? 0) + }, call: call, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|background", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) let overlayParams = ViewType.Overlay.Params(alignment: alignment) let medium = self.medium.resettingViewModifiers() .appending(viewModifier: overlayParams) let content = try Inspector.unwrap(content: Content(rootView, medium: medium)) - let call = ViewType.inspectionCall(base: "background(\(ViewType.indexPlaceholder))", index: index) + let base = call + "(\(ViewType.indexPlaceholder))" + let call = ViewType.inspectionCall(base: base, index: index) return try .init(content, parent: parent, call: call, index: index) } } diff --git a/Tests/ViewInspectorTests/SwiftUI/DelayedPreferenceViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/DelayedPreferenceViewTests.swift deleted file mode 100644 index 673d4e43..00000000 --- a/Tests/ViewInspectorTests/SwiftUI/DelayedPreferenceViewTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -import SwiftUI -@testable import ViewInspector - -#if os(iOS) || os(tvOS) - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -final class DelayedPreferenceViewTests: XCTestCase { - - func testUnwrapDelayedPreferenceView() throws { - let view = Group { - Text("Test") - .backgroundPreferenceValue(Key.self) { _ in EmptyView() } - } - // Not supported - // swiftlint:disable line_length - XCTAssertThrows( - try view.inspect().group().text(0), - "'PreferenceValue' modifiers are currently not supported. Consider extracting the enclosed view for direct inspection.") - // swiftlint:enable line_length - } - - func testRetainsModifiers() throws { - /* Disabled until supported - - let view = Text("Test") - .padding() - .backgroundPreferenceValue(Key.self) { _ in EmptyView() } - .padding().padding() - let sut = try view.inspect().text() - XCTAssertEqual(sut.content.medium.viewModifiers.count, 3) - */ - } - - struct Key: PreferenceKey { - static var defaultValue: String = "test" - static func reduce(value: inout String, nextValue: () -> String) { - value = nextValue() - } - } -} - -#endif diff --git a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift new file mode 100644 index 00000000..86fd5716 --- /dev/null +++ b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift @@ -0,0 +1,68 @@ +import XCTest +import SwiftUI +@testable import ViewInspector + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +final class PreferenceTests: XCTestCase { + + func testPreference() throws { + let sut = EmptyView().preference(key: Key.self, value: "test") + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testTransformPreference() throws { + let sut = EmptyView().transformPreference(Key.self) { _ in } + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testAnchorPreference() throws { + let source = Anchor.Source([Anchor.Source]()) + let sut = EmptyView().anchorPreference(key: Key.self, value: source, transform: { _ in "" }) + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testTransformAnchorPreference() throws { + let source = Anchor.Source([Anchor.Source]()) + let sut = EmptyView().transformAnchorPreference(key: Key.self, value: source, transform: { _, _ in }) + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testOnPreferenceChange() throws { + let sut = EmptyView().onPreferenceChange(Key.self) { _ in } + XCTAssertNoThrow(try sut.inspect().emptyView()) + } + + func testOverlayPreferenceValue() throws { + let sut = try EmptyView() + .overlayPreferenceValue(Key.self) { _ in Text("Test") } + .inspect() + XCTAssertNoThrow(try sut.emptyView()) + XCTAssertEqual(try sut.overlayPreferenceValue().text().string(), "Test") + } + + func testBackgroundPreferenceValue() throws { + let sut = try EmptyView() + .backgroundPreferenceValue(Key.self) { _ in Text("Test") } + .inspect() + XCTAssertNoThrow(try sut.emptyView()) + XCTAssertEqual(try sut.backgroundPreferenceValue().text().string(), "Test") + } + + func testRetainsModifiers() throws { + let view = Text("Test") + .padding() + .overlayPreferenceValue(Key.self) { _ in EmptyView().padding() } + .padding().padding() + let sut1 = try view.inspect().text() + XCTAssertEqual(sut1.content.medium.viewModifiers.count, 4) + let sut2 = try view.inspect().overlayPreferenceValue().emptyView() + XCTAssertEqual(sut2.content.medium.viewModifiers.count, 1) + } + + struct Key: PreferenceKey { + static var defaultValue: String = "abc" + static func reduce(value: inout String, nextValue: () -> String) { + value = nextValue() + } + } +} diff --git a/Tests/ViewInspectorTests/ViewModifiers/EnvironmentModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/EnvironmentModifiersTests.swift index 1736f374..666cd80a 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/EnvironmentModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/EnvironmentModifiersTests.swift @@ -43,63 +43,3 @@ private extension EnvironmentValues { set { self[TestEnvKey.self] = newValue } } } - -// MARK: - ViewPreferenceTests - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -final class ViewPreferenceTests: XCTestCase { - - func testPreference() throws { - let sut = EmptyView().preference(key: Key.self, value: "test") - XCTAssertNoThrow(try sut.inspect().emptyView()) - } - - func testTransformPreference() throws { - let sut = EmptyView().transformPreference(Key.self) { _ in } - XCTAssertNoThrow(try sut.inspect().emptyView()) - } - - func testAnchorPreference() throws { - let source = Anchor.Source([Anchor.Source]()) - let sut = EmptyView().anchorPreference(key: Key.self, value: source, transform: { _ in "" }) - XCTAssertNoThrow(try sut.inspect().emptyView()) - } - - func testTransformAnchorPreference() throws { - let source = Anchor.Source([Anchor.Source]()) - let sut = EmptyView().transformAnchorPreference(key: Key.self, value: source, transform: { _, _ in }) - XCTAssertNoThrow(try sut.inspect().emptyView()) - } - - func testOnPreferenceChange() throws { - let sut = EmptyView().onPreferenceChange(Key.self) { _ in } - XCTAssertNoThrow(try sut.inspect().emptyView()) - } - - func testBackgroundPreferenceValue() throws { - let sut = EmptyView().backgroundPreferenceValue(Key.self) { _ in Text("") } - // Not supported - // swiftlint:disable line_length - XCTAssertThrows( - try sut.inspect().emptyView(), - "'PreferenceValue' modifiers are currently not supported. Consider extracting the enclosed view for direct inspection.") - // swiftlint:enable line_length - } - - func testOverlayPreferenceValue() throws { - let sut = EmptyView().overlayPreferenceValue(Key.self) { _ in Text("") } - // Not supported - // swiftlint:disable line_length - XCTAssertThrows( - try sut.inspect().emptyView(), - "'PreferenceValue' modifiers are currently not supported. Consider extracting the enclosed view for direct inspection.") - // swiftlint:enable line_length - } - - struct Key: PreferenceKey { - static var defaultValue: String = "abc" - static func reduce(value: inout String, nextValue: () -> String) { - value = nextValue() - } - } -} diff --git a/ViewInspector.xcodeproj/project.pbxproj b/ViewInspector.xcodeproj/project.pbxproj index cb72f07b..2ee00c24 100644 --- a/ViewInspector.xcodeproj/project.pbxproj +++ b/ViewInspector.xcodeproj/project.pbxproj @@ -89,7 +89,7 @@ F609EFBD23A40B8800B9256A /* DelayedPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFBC23A40B8800B9256A /* DelayedPreferenceView.swift */; }; F609EFBF23A424D800B9256A /* EnvironmentModifiersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFBE23A424D800B9256A /* EnvironmentModifiersTests.swift */; }; F609EFC123A432AA00B9256A /* IDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFC023A432AA00B9256A /* IDView.swift */; }; - F609EFC323A4342400B9256A /* DelayedPreferenceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFC223A4342400B9256A /* DelayedPreferenceViewTests.swift */; }; + F609EFC323A4342400B9256A /* PreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFC223A4342400B9256A /* PreferenceTests.swift */; }; F609EFC523A4350700B9256A /* IDViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F609EFC423A4350700B9256A /* IDViewTests.swift */; }; F60EEBCD2382EED0007DB53A /* Inspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60EEBBA2382EED0007DB53A /* Inspector.swift */; }; F60EEBCE2382EED0007DB53A /* BaseTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60EEBBB2382EED0007DB53A /* BaseTypes.swift */; }; @@ -342,7 +342,7 @@ F609EFBC23A40B8800B9256A /* DelayedPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedPreferenceView.swift; sourceTree = ""; }; F609EFBE23A424D800B9256A /* EnvironmentModifiersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentModifiersTests.swift; sourceTree = ""; }; F609EFC023A432AA00B9256A /* IDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDView.swift; sourceTree = ""; }; - F609EFC223A4342400B9256A /* DelayedPreferenceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedPreferenceViewTests.swift; sourceTree = ""; }; + F609EFC223A4342400B9256A /* PreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceTests.swift; sourceTree = ""; }; F609EFC423A4350700B9256A /* IDViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDViewTests.swift; sourceTree = ""; }; F60EEBBA2382EED0007DB53A /* Inspector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inspector.swift; sourceTree = ""; }; F60EEBBB2382EED0007DB53A /* BaseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTypes.swift; sourceTree = ""; }; @@ -733,7 +733,6 @@ F6B7D0092494E12F00ABB5E0 /* CustomViewBuilderTests.swift */, F69C91CB2383189200515A91 /* CustomViewModifierTests.swift */, F60EEBF32382F004007DB53A /* DatePickerTests.swift */, - F609EFC223A4342400B9256A /* DelayedPreferenceViewTests.swift */, F682A00B254772C3005F1B70 /* DisclosureGroupTests.swift */, F60EEBEC2382F004007DB53A /* DividerTests.swift */, F6DFD87D2382FBE80028E84D /* EditButtonTests.swift */, @@ -772,6 +771,7 @@ F6684BF323AA55C100DECCB3 /* PasteButtonTests.swift */, F6D933B82385EDDF00358E0E /* PickerTests.swift */, F6BD82092565AD5D00A772D4 /* PopoverTests.swift */, + F609EFC223A4342400B9256A /* PreferenceTests.swift */, F6C15A98254D9F24000240F1 /* ProgressViewTests.swift */, F6684BFF23AA864F00DECCB3 /* RadialGradientTests.swift */, 52B71AA526EB5BED00B719D4 /* SafeAreaInsetTests.swift */, @@ -1198,7 +1198,7 @@ F6C15A3F254B4835000240F1 /* LinkTests.swift in Sources */, F6ECF6D823A69CB5000FC591 /* VisualEffectModifiersTests.swift in Sources */, F6DB5A0B253510CC0056FC83 /* TextInputModifiersTests.swift in Sources */, - F609EFC323A4342400B9256A /* DelayedPreferenceViewTests.swift in Sources */, + F609EFC323A4342400B9256A /* PreferenceTests.swift in Sources */, F6F08E6423A29FA1001F04DF /* SubscriptionViewTests.swift in Sources */, F6D933AD2385EB0100358E0E /* GroupBoxTests.swift in Sources */, F6ECF6D223A680DA000FC591 /* TransformingModifiersTests.swift in Sources */, From 62ce2e0fe7aedc9c2ee0c9f873df48aa79cf8f13 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 22:58:51 +0300 Subject: [PATCH 19/54] Refactor protocols for view extraction --- Sources/ViewInspector/BaseTypes.swift | 17 +++++++++++++++++ .../SwiftUI/DelayedPreferenceView.swift | 18 ++++-------------- Sources/ViewInspector/SwiftUI/ForEach.swift | 9 ++------- .../ViewInspector/SwiftUI/GeometryReader.swift | 8 ++------ .../ViewInspector/SwiftUI/OutlineGroup.swift | 8 ++------ .../SwiftUI/ScrollViewReader.swift | 8 ++------ 6 files changed, 29 insertions(+), 39 deletions(-) diff --git a/Sources/ViewInspector/BaseTypes.swift b/Sources/ViewInspector/BaseTypes.swift index 2db51bb3..11b67c95 100644 --- a/Sources/ViewInspector/BaseTypes.swift +++ b/Sources/ViewInspector/BaseTypes.swift @@ -284,6 +284,23 @@ extension InspectionError: CustomStringConvertible, LocalizedError { } } +// MARK: - ViewProvider + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal protocol SingleViewProvider { + func view() throws -> Any +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal protocol MultipleViewProvider { + func views() throws -> LazyGroup +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal protocol ElementViewProvider { + func view(_ element: Any) throws -> Any +} + // MARK: - BinaryEquatable @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) diff --git a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift index 829cf0ec..06535ab9 100644 --- a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift +++ b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift @@ -26,7 +26,7 @@ internal extension ViewType { extension ViewType.DelayedPreferenceView: SingleViewContent { static func child(_ content: Content) throws -> Content { - let provider = try Inspector.cast(value: content.view, type: DelayedPreferenceContentProvider.self) + let provider = try Inspector.cast(value: content.view, type: SingleViewProvider.self) let view = try provider.view() return try Inspector.unwrap(content: Content(view, medium: content.medium)) } @@ -39,7 +39,7 @@ extension ViewType.PreferenceReadingView: SingleViewContent { static func child(_ content: Content) throws -> Content { let provider = try Inspector.cast( - value: content.view, type: PreferenceReadingViewContentProvider.self) + value: content.view, type: SingleViewProvider.self) let view = try provider.view() let medium = content.medium.resettingViewModifiers() return try Inspector.unwrap(content: Content(view, medium: medium)) @@ -47,12 +47,7 @@ extension ViewType.PreferenceReadingView: SingleViewContent { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private protocol DelayedPreferenceContentProvider { - func view() throws -> Any -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension _DelayedPreferenceView: DelayedPreferenceContentProvider { +extension _DelayedPreferenceView: SingleViewProvider { func view() throws -> Any { typealias Builder = (_PreferenceValue) -> Content let readingViewBuilder = try Inspector.attribute(label: "transform", value: self, type: Builder.self) @@ -76,12 +71,7 @@ private extension _PreferenceValue { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private protocol PreferenceReadingViewContentProvider { - func view() throws -> Any -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension _PreferenceReadingView: PreferenceReadingViewContentProvider { +extension _PreferenceReadingView: SingleViewProvider { func view() throws -> Any { typealias Builder = (Key.Value) -> Content let builder = try Inspector.attribute(label: "transform", value: self, type: Builder.self) diff --git a/Sources/ViewInspector/SwiftUI/ForEach.swift b/Sources/ViewInspector/SwiftUI/ForEach.swift index 15a7b244..da8e91cb 100644 --- a/Sources/ViewInspector/SwiftUI/ForEach.swift +++ b/Sources/ViewInspector/SwiftUI/ForEach.swift @@ -15,7 +15,7 @@ public extension ViewType { extension ViewType.ForEach: MultipleViewContent { public static func children(_ content: Content) throws -> LazyGroup { - let provider = try Inspector.cast(value: content.view, type: ForEachContentProvider.self) + let provider = try Inspector.cast(value: content.view, type: MultipleViewProvider.self) let children = try provider.views() return LazyGroup(count: children.count) { index in try Inspector.unwrap(view: try children.element(at: index), @@ -85,12 +85,7 @@ public extension InspectableView where View == ViewType.ForEach { // MARK: - Private @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private protocol ForEachContentProvider { - func views() throws -> LazyGroup -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension ForEach: ForEachContentProvider { +extension ForEach: MultipleViewProvider { func views() throws -> LazyGroup { diff --git a/Sources/ViewInspector/SwiftUI/GeometryReader.swift b/Sources/ViewInspector/SwiftUI/GeometryReader.swift index 85368092..30dc5f7c 100644 --- a/Sources/ViewInspector/SwiftUI/GeometryReader.swift +++ b/Sources/ViewInspector/SwiftUI/GeometryReader.swift @@ -14,7 +14,7 @@ public extension ViewType { extension ViewType.GeometryReader: SingleViewContent { public static func child(_ content: Content) throws -> Content { - let provider = try Inspector.cast(value: content.view, type: GeometryReaderContentProvider.self) + let provider = try Inspector.cast(value: content.view, type: SingleViewProvider.self) let medium = content.medium.resettingViewModifiers() return try Inspector.unwrap(view: provider.view(), medium: medium) } @@ -42,12 +42,8 @@ public extension InspectableView where View: MultipleViewContent { // MARK: - Private -private protocol GeometryReaderContentProvider { - func view() throws -> Any -} - @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension GeometryReader: GeometryReaderContentProvider { +extension GeometryReader: SingleViewProvider { func view() throws -> Any { typealias Builder = (GeometryProxy) -> Content let builder = try Inspector diff --git a/Sources/ViewInspector/SwiftUI/OutlineGroup.swift b/Sources/ViewInspector/SwiftUI/OutlineGroup.swift index 6d2c4e03..eeb25c47 100644 --- a/Sources/ViewInspector/SwiftUI/OutlineGroup.swift +++ b/Sources/ViewInspector/SwiftUI/OutlineGroup.swift @@ -46,7 +46,7 @@ public extension InspectableView where View == ViewType.OutlineGroup { } func leaf(_ dataElement: Any) throws -> InspectableView { - let provider = try Inspector.cast(value: content.view, type: LeafContentProvider.self) + let provider = try Inspector.cast(value: content.view, type: ElementViewProvider.self) let medium = content.medium.resettingViewModifiers() return try .init(Content(try provider.view(dataElement), medium: medium), parent: self) } @@ -54,15 +54,11 @@ public extension InspectableView where View == ViewType.OutlineGroup { // MARK: - Private -private protocol LeafContentProvider { - func view(_ element: Any) throws -> Any -} - #if os(iOS) || os(macOS) @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -extension OutlineGroup: LeafContentProvider { +extension OutlineGroup: ElementViewProvider { func view(_ element: Any) throws -> Any { guard let data = element as? Data.Element else { throw InspectionError.typeMismatch(element, Data.Element.self) diff --git a/Sources/ViewInspector/SwiftUI/ScrollViewReader.swift b/Sources/ViewInspector/SwiftUI/ScrollViewReader.swift index 6b6a4805..7ae10a60 100644 --- a/Sources/ViewInspector/SwiftUI/ScrollViewReader.swift +++ b/Sources/ViewInspector/SwiftUI/ScrollViewReader.swift @@ -34,7 +34,7 @@ public extension InspectableView where View: MultipleViewContent { extension ViewType.ScrollViewReader: SingleViewContent { public static func child(_ content: Content) throws -> Content { - let provider = try Inspector.cast(value: content.view, type: ScrollViewReaderContentProvider.self) + let provider = try Inspector.cast(value: content.view, type: SingleViewProvider.self) let medium = content.medium.resettingViewModifiers() return try Inspector.unwrap(view: provider.view(), medium: medium) } @@ -42,12 +42,8 @@ extension ViewType.ScrollViewReader: SingleViewContent { // MARK: - Private -private protocol ScrollViewReaderContentProvider { - func view() throws -> Any -} - @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) -extension ScrollViewReader: ScrollViewReaderContentProvider { +extension ScrollViewReader: SingleViewProvider { func view() throws -> Any { typealias Builder = (ScrollViewProxy) -> Content let builder = try Inspector From 193acc5f476561b66809898744f1fe91fd0ac8bb Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 23:15:52 +0300 Subject: [PATCH 20/54] Refactor overlay and background extraction --- .../SwiftUI/DelayedPreferenceView.swift | 4 +-- Sources/ViewInspector/SwiftUI/Overlay.swift | 26 +++++++++++++------ Sources/ViewInspector/ViewSearchIndex.swift | 4 +-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift index 06535ab9..ea9dbf06 100644 --- a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift +++ b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift @@ -5,12 +5,12 @@ public extension InspectableView { func overlayPreferenceValue(_ index: Int? = nil) throws -> InspectableView { return try contentForModifierLookup - .overlay(parent: self, call: "overlayPreferenceValue", index: index) + .overlay(parent: self, api: .overlayPreferenceValue, index: index) } func backgroundPreferenceValue(_ index: Int? = nil) throws -> InspectableView { return try contentForModifierLookup - .background(parent: self, call: "backgroundPreferenceValue", index: index) + .background(parent: self, api: .backgroundPreferenceValue, index: index) } } diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index 702af0a7..e384aecc 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -15,11 +15,11 @@ public extension ViewType { public extension InspectableView { func overlay(_ index: Int? = nil) throws -> InspectableView { - return try contentForModifierLookup.overlay(parent: self, index: index) + return try contentForModifierLookup.overlay(parent: self, api: .overlay, index: index) } func background(_ index: Int? = nil) throws -> InspectableView { - return try contentForModifierLookup.background(parent: self, index: index) + return try contentForModifierLookup.background(parent: self, api: .background, index: index) } } @@ -44,34 +44,44 @@ extension ViewType.Overlay: MultipleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension Content { - func overlay(parent: UnwrappedView, call: String = "overlay", index: Int? + enum OverlayAPI: String { + case overlay + case overlayPreferenceValue + } + + func overlay(parent: UnwrappedView, api: OverlayAPI, index: Int? ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in return modifier.modifierType.contains("_OverlayModifier") - }, call: call, index: index ?? 0) + }, call: api.rawValue, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) let overlayParams = ViewType.Overlay.Params(alignment: alignment) let medium = self.medium.resettingViewModifiers() .appending(viewModifier: overlayParams) let content = try Inspector.unwrap(content: Content(rootView, medium: medium)) - let base = call + "(\(ViewType.indexPlaceholder))" + let base = api.rawValue + "(\(ViewType.indexPlaceholder))" let call = ViewType.inspectionCall(base: base, index: index) return try .init(content, parent: parent, call: call, index: index) } - func background(parent: UnwrappedView, call: String = "background", index: Int? + enum BackgroundAPI: String { + case background + case backgroundPreferenceValue + } + + func background(parent: UnwrappedView, api: BackgroundAPI, index: Int? ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in return modifier.modifierType.contains("_BackgroundModifier") - }, call: call, index: index ?? 0) + }, call: api.rawValue, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|background", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) let overlayParams = ViewType.Overlay.Params(alignment: alignment) let medium = self.medium.resettingViewModifiers() .appending(viewModifier: overlayParams) let content = try Inspector.unwrap(content: Content(rootView, medium: medium)) - let base = call + "(\(ViewType.indexPlaceholder))" + let base = api.rawValue + "(\(ViewType.indexPlaceholder))" let call = ViewType.inspectionCall(base: base, index: index) return try .init(content, parent: parent, call: call, index: index) } diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index b89e38d1..d493ac51 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -254,10 +254,10 @@ internal extension ViewSearch { static private(set) var modifierIdentities: [ModifierIdentity] = [ .init(name: "_OverlayModifier", builder: { parent, index in - try parent.content.overlay(parent: parent, index: index) + try parent.content.overlay(parent: parent, api: .overlay, index: index) }), .init(name: "_BackgroundModifier", builder: { parent, index in - try parent.content.background(parent: parent, index: index) + try parent.content.background(parent: parent, api: .background, index: index) }), .init(name: ViewType.Toolbar.typePrefix, builder: { parent, index in try parent.content.toolbar(parent: parent, index: index) From cb67b82f7910609ccae6ec97d5327db23a8fd730 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Tue, 21 Dec 2021 23:47:11 +0300 Subject: [PATCH 21/54] Refactor border inspection --- .../Modifiers/VisualEffectModifiers.swift | 16 +++++++--------- Sources/ViewInspector/SwiftUI/Overlay.swift | 8 ++++++-- Sources/ViewInspector/ViewSearchIndex.swift | 4 ++-- .../ViewModifiers/NestedModifiersTests.swift | 2 +- .../VisualEffectModifiersTests.swift | 5 ++++- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Sources/ViewInspector/Modifiers/VisualEffectModifiers.swift b/Sources/ViewInspector/Modifiers/VisualEffectModifiers.swift index 46c76d0a..33ee3b93 100644 --- a/Sources/ViewInspector/Modifiers/VisualEffectModifiers.swift +++ b/Sources/ViewInspector/Modifiers/VisualEffectModifiers.swift @@ -82,15 +82,13 @@ public extension InspectableView { return (color, radius, offset) } - func border(_ type: S.Type) throws -> (content: S, width: CGFloat) { - let content = try modifierAttribute(modifierName: - "_OverlayModifier<_ShapeView<_StrokedShape", path: "modifier|overlay|style", - type: Any.self, call: "border") - let castedContent = try Inspector.cast(value: content, type: S.self) - let width = try modifierAttribute(modifierName: - "_OverlayModifier<_ShapeView<_StrokedShape", path: "modifier|overlay|shape|style|lineWidth", - type: CGFloat.self, call: "border") - return (castedContent, width) + func border(_ style: S.Type) throws -> (shapeStyle: S, width: CGFloat) { + let shape = try contentForModifierLookup + .overlay(parent: self, api: .border, index: nil) + .shape() + let shapeStyle = try shape.fillShapeStyle(style) + let width = try shape.strokeStyle().lineWidth + return (shapeStyle, width) } func blendMode() throws -> BlendMode { diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index e384aecc..3c2cbb94 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -6,6 +6,9 @@ public extension ViewType { struct Overlay: KnownViewType { public static var typePrefix: String = "" public static var isTransitive: Bool { true } + + internal static var overlayModifierName: String { "_OverlayModifier" } + internal static var backgroundModifierName: String { "_BackgroundModifier" } } } @@ -45,6 +48,7 @@ extension ViewType.Overlay: MultipleViewContent { internal extension Content { enum OverlayAPI: String { + case border case overlay case overlayPreferenceValue } @@ -52,7 +56,7 @@ internal extension Content { func overlay(parent: UnwrappedView, api: OverlayAPI, index: Int? ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in - return modifier.modifierType.contains("_OverlayModifier") + return modifier.modifierType.contains(ViewType.Overlay.overlayModifierName) }, call: api.rawValue, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) @@ -73,7 +77,7 @@ internal extension Content { func background(parent: UnwrappedView, api: BackgroundAPI, index: Int? ) throws -> InspectableView { let modifier = try self.modifier({ modifier -> Bool in - return modifier.modifierType.contains("_BackgroundModifier") + return modifier.modifierType.contains(ViewType.Overlay.backgroundModifierName) }, call: api.rawValue, index: index ?? 0) let rootView = try Inspector.attribute(path: "modifier|background", value: modifier) let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index d493ac51..ffae53fc 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -253,10 +253,10 @@ private extension Content { internal extension ViewSearch { static private(set) var modifierIdentities: [ModifierIdentity] = [ - .init(name: "_OverlayModifier", builder: { parent, index in + .init(name: ViewType.Overlay.overlayModifierName, builder: { parent, index in try parent.content.overlay(parent: parent, api: .overlay, index: index) }), - .init(name: "_BackgroundModifier", builder: { parent, index in + .init(name: ViewType.Overlay.backgroundModifierName, builder: { parent, index in try parent.content.background(parent: parent, api: .background, index: index) }), .init(name: ViewType.Toolbar.typePrefix, builder: { parent, index in diff --git a/Tests/ViewInspectorTests/ViewModifiers/NestedModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/NestedModifiersTests.swift index bfc8e60f..30306089 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/NestedModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/NestedModifiersTests.swift @@ -10,7 +10,7 @@ final class NestedModifiersTests: XCTestCase { .border(Color.red, width: 2) .border(Color.blue, width: 3) let sut = try view.inspect().emptyView().border(Color.self) - XCTAssertEqual(sut.content, .red) + XCTAssertEqual(sut.shapeStyle, .red) XCTAssertEqual(sut.width, 2) } } diff --git a/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift index c4e68e79..ba09aac3 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift @@ -137,8 +137,11 @@ final class ViewGraphicalEffectsTests: XCTestCase { startPoint: .bottom, endPoint: .top) let sut = try EmptyView().border(gradient, width: 7) .inspect().emptyView().border(LinearGradient.self) - XCTAssertEqual(sut.content, gradient) + XCTAssertEqual(sut.shapeStyle, gradient) XCTAssertEqual(sut.width, 7) + let sut2 = try EmptyView().padding().inspect() + XCTAssertThrows(try sut2.border(LinearGradient.self), + "EmptyView does not have 'border' modifier") } func testBlendMode() throws { From fc3cbb7f4cb80974c46e96584afa6d38c814daa2 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 01:31:26 +0300 Subject: [PATCH 22/54] Distinguish overlay, overlayPreferenceValue and border --- Sources/ViewInspector/SwiftUI/Overlay.swift | 46 +++++++++++++++++-- .../SwiftUI/PreferenceTests.swift | 22 +++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index 3c2cbb94..e54bdfcc 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -55,10 +55,20 @@ internal extension Content { func overlay(parent: UnwrappedView, api: OverlayAPI, index: Int? ) throws -> InspectableView { - let modifier = try self.modifier({ modifier -> Bool in - return modifier.modifierType.contains(ViewType.Overlay.overlayModifierName) - }, call: api.rawValue, index: index ?? 0) - let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) + let modifiers = modifiersMatching { + $0.modifierType.contains(ViewType.Overlay.overlayModifierName) + } + guard let (modifier, rootView) = modifiers.lazy.compactMap({ modifier -> (Any, Any)? in + do { + let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) + try api.verifySignature(of: rootView, parent: view, index: index) + return (modifier, rootView) + } catch { return nil } + }).dropFirst(index ?? 0).first else { + let parentName = Inspector.typeName(value: view) + throw InspectionError.modifierNotFound( + parent: parentName, modifier: api.rawValue, index: index ?? 0) + } let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) let overlayParams = ViewType.Overlay.Params(alignment: alignment) let medium = self.medium.resettingViewModifiers() @@ -91,6 +101,34 @@ internal extension Content { } } +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal extension Content.OverlayAPI { + + func verifySignature(of content: Any, parent: Any, index: Int?) throws { + let reportFailure: () throws -> Void = { + throw InspectionError.notSupported("Different view signature") + } + switch self { + case .border: + let stroke = try? InspectableView(Content(content), parent: nil, index: nil).strokeStyle() + if stroke == nil { + try reportFailure() + } + case .overlay: + let otherCases = [Content.OverlayAPI.border, .overlayPreferenceValue] + if otherCases.contains(where: { + (try? $0.verifySignature(of: content, parent: parent, index: index)) != nil + }) { + try reportFailure() + } + case .overlayPreferenceValue: + if Inspector.typeName(value: content, generics: .remove) != "_PreferenceReadingView" { + try reportFailure() + } + } + } +} + // MARK: - Custom Attributes @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) diff --git a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift index 86fd5716..e0cc501d 100644 --- a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift @@ -65,4 +65,26 @@ final class PreferenceTests: XCTestCase { value = nextValue() } } + + func testDifferentOverlayUseCases() throws { + let sut = try ManyOverlaysView().inspect().emptyView() + let prefValue = try sut.overlayPreferenceValue().text().string() + XCTAssertEqual(prefValue, Key.defaultValue) + let border = try sut.border(Color.self) + XCTAssertEqual(border.shapeStyle, .red) + XCTAssertNoThrow(try sut.overlay(1).spacer()) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct ManyOverlaysView: View, Inspectable { + var body: some View { + EmptyView() + .overlay(AnyView(EmptyView())) + .overlayPreferenceValue(PreferenceTests.Key.self) { value in + Text(value) + } + .border(Color.red) + .overlay(Spacer()) + } } From f348a6d1c00559edc881e06b73f4c39a8e921962 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 12:30:57 +0300 Subject: [PATCH 23/54] Fix border vs overlay inspection issue --- Sources/ViewInspector/SwiftUI/Overlay.swift | 9 +++++---- .../VisualEffectModifiersTests.swift | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index e54bdfcc..56215475 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -58,10 +58,11 @@ internal extension Content { let modifiers = modifiersMatching { $0.modifierType.contains(ViewType.Overlay.overlayModifierName) } + let hasMultipleOverlays = modifiers.count > 1 guard let (modifier, rootView) = modifiers.lazy.compactMap({ modifier -> (Any, Any)? in do { let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) - try api.verifySignature(of: rootView, parent: view, index: index) + try api.verifySignature(content: rootView, hasMultipleOverlays: hasMultipleOverlays) return (modifier, rootView) } catch { return nil } }).dropFirst(index ?? 0).first else { @@ -104,7 +105,7 @@ internal extension Content { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension Content.OverlayAPI { - func verifySignature(of content: Any, parent: Any, index: Int?) throws { + func verifySignature(content: Any, hasMultipleOverlays: Bool) throws { let reportFailure: () throws -> Void = { throw InspectionError.notSupported("Different view signature") } @@ -116,8 +117,8 @@ internal extension Content.OverlayAPI { } case .overlay: let otherCases = [Content.OverlayAPI.border, .overlayPreferenceValue] - if otherCases.contains(where: { - (try? $0.verifySignature(of: content, parent: parent, index: index)) != nil + if hasMultipleOverlays, otherCases.contains(where: { + (try? $0.verifySignature(content: content, hasMultipleOverlays: hasMultipleOverlays)) != nil }) { try reportFailure() } diff --git a/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift index ba09aac3..dca511f9 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/VisualEffectModifiersTests.swift @@ -135,12 +135,20 @@ final class ViewGraphicalEffectsTests: XCTestCase { func testBorderInspection() throws { let gradient = LinearGradient(gradient: Gradient(colors: [.red]), startPoint: .bottom, endPoint: .top) - let sut = try EmptyView().border(gradient, width: 7) + let sut1 = try EmptyView().border(gradient, width: 7) .inspect().emptyView().border(LinearGradient.self) - XCTAssertEqual(sut.shapeStyle, gradient) - XCTAssertEqual(sut.width, 7) - let sut2 = try EmptyView().padding().inspect() - XCTAssertThrows(try sut2.border(LinearGradient.self), + XCTAssertEqual(sut1.shapeStyle, gradient) + XCTAssertEqual(sut1.width, 7) + + let borderSimulator = Rectangle() + .strokeBorder(Color.red, lineWidth: 3, antialiased: true) + let sut2 = try EmptyView().overlay(borderSimulator).inspect() + let border = try sut2.border(Color.self) + XCTAssertEqual(border.width, 3) + XCTAssertNoThrow(try sut2.overlay().shape()) + + let sut3 = EmptyView().padding() + XCTAssertThrows(try sut3.inspect().border(LinearGradient.self), "EmptyView does not have 'border' modifier") } From b78c64461c5412ef676727b0d88b9d1077cc6385 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 12:48:11 +0300 Subject: [PATCH 24/54] Unify overlay and background inspection --- .../SwiftUI/DelayedPreferenceView.swift | 2 +- Sources/ViewInspector/SwiftUI/Overlay.swift | 74 ++++++++++--------- Sources/ViewInspector/ViewSearchIndex.swift | 13 ++-- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift index ea9dbf06..5037bce2 100644 --- a/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift +++ b/Sources/ViewInspector/SwiftUI/DelayedPreferenceView.swift @@ -10,7 +10,7 @@ public extension InspectableView { func backgroundPreferenceValue(_ index: Int? = nil) throws -> InspectableView { return try contentForModifierLookup - .background(parent: self, api: .backgroundPreferenceValue, index: index) + .overlay(parent: self, api: .backgroundPreferenceValue, index: index) } } diff --git a/Sources/ViewInspector/SwiftUI/Overlay.swift b/Sources/ViewInspector/SwiftUI/Overlay.swift index 56215475..365f9e5d 100644 --- a/Sources/ViewInspector/SwiftUI/Overlay.swift +++ b/Sources/ViewInspector/SwiftUI/Overlay.swift @@ -6,9 +6,6 @@ public extension ViewType { struct Overlay: KnownViewType { public static var typePrefix: String = "" public static var isTransitive: Bool { true } - - internal static var overlayModifierName: String { "_OverlayModifier" } - internal static var backgroundModifierName: String { "_BackgroundModifier" } } } @@ -22,7 +19,7 @@ public extension InspectableView { } func background(_ index: Int? = nil) throws -> InspectableView { - return try contentForModifierLookup.background(parent: self, api: .background, index: index) + return try contentForModifierLookup.overlay(parent: self, api: .background, index: index) } } @@ -47,21 +44,15 @@ extension ViewType.Overlay: MultipleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension Content { - enum OverlayAPI: String { - case border - case overlay - case overlayPreferenceValue - } - - func overlay(parent: UnwrappedView, api: OverlayAPI, index: Int? + func overlay(parent: UnwrappedView, api: ViewType.Overlay.API, index: Int? ) throws -> InspectableView { let modifiers = modifiersMatching { - $0.modifierType.contains(ViewType.Overlay.overlayModifierName) + $0.modifierType.contains(api.modifierName) } let hasMultipleOverlays = modifiers.count > 1 guard let (modifier, rootView) = modifiers.lazy.compactMap({ modifier -> (Any, Any)? in do { - let rootView = try Inspector.attribute(path: "modifier|overlay", value: modifier) + let rootView = try Inspector.attribute(path: api.rootViewPath, value: modifier) try api.verifySignature(content: rootView, hasMultipleOverlays: hasMultipleOverlays) return (modifier, rootView) } catch { return nil } @@ -79,31 +70,41 @@ internal extension Content { let call = ViewType.inspectionCall(base: base, index: index) return try .init(content, parent: parent, call: call, index: index) } - - enum BackgroundAPI: String { +} + +// MARK: - ViewType.Overlay.API + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal extension ViewType.Overlay { + enum API: String { + case border + case overlay + case overlayPreferenceValue case background case backgroundPreferenceValue } - - func background(parent: UnwrappedView, api: BackgroundAPI, index: Int? - ) throws -> InspectableView { - let modifier = try self.modifier({ modifier -> Bool in - return modifier.modifierType.contains(ViewType.Overlay.backgroundModifierName) - }, call: api.rawValue, index: index ?? 0) - let rootView = try Inspector.attribute(path: "modifier|background", value: modifier) - let alignment = try Inspector.attribute(path: "modifier|alignment", value: modifier, type: Alignment.self) - let overlayParams = ViewType.Overlay.Params(alignment: alignment) - let medium = self.medium.resettingViewModifiers() - .appending(viewModifier: overlayParams) - let content = try Inspector.unwrap(content: Content(rootView, medium: medium)) - let base = api.rawValue + "(\(ViewType.indexPlaceholder))" - let call = ViewType.inspectionCall(base: base, index: index) - return try .init(content, parent: parent, call: call, index: index) - } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -internal extension Content.OverlayAPI { +internal extension ViewType.Overlay.API { + + var modifierName: String { + switch self { + case .border, .overlay, .overlayPreferenceValue: + return "_OverlayModifier" + case .background, .backgroundPreferenceValue: + return "_BackgroundModifier" + } + } + + var rootViewPath: String { + switch self { + case .border, .overlay, .overlayPreferenceValue: + return "modifier|overlay" + case .background, .backgroundPreferenceValue: + return "modifier|background" + } + } func verifySignature(content: Any, hasMultipleOverlays: Bool) throws { let reportFailure: () throws -> Void = { @@ -116,13 +117,18 @@ internal extension Content.OverlayAPI { try reportFailure() } case .overlay: - let otherCases = [Content.OverlayAPI.border, .overlayPreferenceValue] + let otherCases = [ViewType.Overlay.API.border, .overlayPreferenceValue] if hasMultipleOverlays, otherCases.contains(where: { (try? $0.verifySignature(content: content, hasMultipleOverlays: hasMultipleOverlays)) != nil }) { try reportFailure() } - case .overlayPreferenceValue: + case .background: + if (try? ViewType.Overlay.API.backgroundPreferenceValue + .verifySignature(content: content, hasMultipleOverlays: hasMultipleOverlays)) != nil { + try reportFailure() + } + case .overlayPreferenceValue, .backgroundPreferenceValue: if Inspector.typeName(value: content, generics: .remove) != "_PreferenceReadingView" { try reportFailure() } diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index ffae53fc..c91327e9 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -252,13 +252,12 @@ private extension Content { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewSearch { - static private(set) var modifierIdentities: [ModifierIdentity] = [ - .init(name: ViewType.Overlay.overlayModifierName, builder: { parent, index in - try parent.content.overlay(parent: parent, api: .overlay, index: index) - }), - .init(name: ViewType.Overlay.backgroundModifierName, builder: { parent, index in - try parent.content.background(parent: parent, api: .background, index: index) - }), + static private(set) var modifierIdentities: [ModifierIdentity] = [ViewType.Overlay.API.overlay, .background] + .map({ api in + ModifierIdentity.init(name: api.modifierName, builder: { parent, index in + try parent.content.overlay(parent: parent, api: api, index: index) + }) + }) + [ .init(name: ViewType.Toolbar.typePrefix, builder: { parent, index in try parent.content.toolbar(parent: parent, index: index) }), From c1cf2069be11126f702ba42d2824c5f4453ccad0 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 12:55:02 +0300 Subject: [PATCH 25/54] Add test for background overlays collision --- .../SwiftUI/PreferenceTests.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift index e0cc501d..3444f194 100644 --- a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift @@ -74,6 +74,13 @@ final class PreferenceTests: XCTestCase { XCTAssertEqual(border.shapeStyle, .red) XCTAssertNoThrow(try sut.overlay(1).spacer()) } + + func testDifferentBackgroundOverlayUseCases() throws { + let sut = try ManyBackgroundOverlaysView().inspect().emptyView() + let prefValue = try sut.backgroundPreferenceValue().text().string() + XCTAssertEqual(prefValue, Key.defaultValue) + XCTAssertNoThrow(try sut.background(1).spacer()) + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) @@ -88,3 +95,15 @@ private struct ManyOverlaysView: View, Inspectable { .overlay(Spacer()) } } + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +private struct ManyBackgroundOverlaysView: View, Inspectable { + var body: some View { + EmptyView() + .background(AnyView(EmptyView())) + .backgroundPreferenceValue(PreferenceTests.Key.self) { value in + Text(value) + } + .background(Spacer()) + } +} From 0b3b0db1a6b9c8000c2782b5d3dee6eb4acd9385 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 13:38:12 +0300 Subject: [PATCH 26/54] Add search support for preferenceValue overlays --- Sources/ViewInspector/ViewSearchIndex.swift | 24 ++++++++++++++----- .../SwiftUI/PreferenceTests.swift | 21 ++++++++++++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index c91327e9..8e175b61 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -249,15 +249,27 @@ private extension Content { // MARK: - ModifierIdentity +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal extension ViewType.Overlay.API { + + static var viewSearchModifierIdentities: [ViewSearch.ModifierIdentity] { + let apiToSearch: [ViewType.Overlay.API] = [ + .overlayPreferenceValue, .backgroundPreferenceValue, + .overlay, .background + ] + return apiToSearch + .map { api in + .init(name: api.modifierName, builder: { parent, index in + try parent.content.overlay(parent: parent, api: api, index: index) + }) + } + } +} + @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewSearch { - static private(set) var modifierIdentities: [ModifierIdentity] = [ViewType.Overlay.API.overlay, .background] - .map({ api in - ModifierIdentity.init(name: api.modifierName, builder: { parent, index in - try parent.content.overlay(parent: parent, api: api, index: index) - }) - }) + [ + static private(set) var modifierIdentities: [ModifierIdentity] = ViewType.Overlay.API.viewSearchModifierIdentities + [ .init(name: ViewType.Toolbar.typePrefix, builder: { parent, index in try parent.content.toolbar(parent: parent, index: index) }), diff --git a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift index 3444f194..14fd2faa 100644 --- a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift @@ -76,18 +76,31 @@ final class PreferenceTests: XCTestCase { } func testDifferentBackgroundOverlayUseCases() throws { - let sut = try ManyBackgroundOverlaysView().inspect().emptyView() + let sut = try ManyBGOverlaysView().inspect().emptyView() let prefValue = try sut.backgroundPreferenceValue().text().string() XCTAssertEqual(prefValue, Key.defaultValue) XCTAssertNoThrow(try sut.background(1).spacer()) } + + func testOverlaySearch() throws { + let sut1 = try ManyOverlaysView().inspect() + XCTAssertEqual(try sut1.find(text: "Test").pathToRoot, + "view(ManyOverlaysView.self).emptyView().overlay().anyView().text()") + XCTAssertEqual(try sut1.find(text: Key.defaultValue).pathToRoot, + "view(ManyOverlaysView.self).emptyView().overlayPreferenceValue().text()") + let sut2 = try ManyBGOverlaysView().inspect() + XCTAssertEqual(try sut2.find(text: "Test").pathToRoot, + "view(ManyBGOverlaysView.self).emptyView().background().anyView().text()") + XCTAssertEqual(try sut2.find(text: Key.defaultValue).pathToRoot, + "view(ManyBGOverlaysView.self).emptyView().backgroundPreferenceValue().text()") + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) private struct ManyOverlaysView: View, Inspectable { var body: some View { EmptyView() - .overlay(AnyView(EmptyView())) + .overlay(AnyView(Text("Test"))) .overlayPreferenceValue(PreferenceTests.Key.self) { value in Text(value) } @@ -97,10 +110,10 @@ private struct ManyOverlaysView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ManyBackgroundOverlaysView: View, Inspectable { +private struct ManyBGOverlaysView: View, Inspectable { var body: some View { EmptyView() - .background(AnyView(EmptyView())) + .background(AnyView(Text("Test"))) .backgroundPreferenceValue(PreferenceTests.Key.self) { value in Text(value) } From 5e3a1afac9bd84a3e5008247d52f5463e1dd6650 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 14:08:31 +0300 Subject: [PATCH 27/54] Update documentation --- guide.md | 14 -------------- readiness.md | 5 ++--- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/guide.md b/guide.md index 044d574c..f30ec2cc 100644 --- a/guide.md +++ b/guide.md @@ -218,20 +218,6 @@ extension InspectableView { let text = try sut.find(textWithFont: .headline) ``` -#### Limitations - -There are a few scenarious when `find` function is unable to automatically traverse the whole view. - -One of such cases is a custom view that does not conform to `Inspectable`. Adding a corresponding extension in the test scope solves this problem. - -In addition to that, there are a few SwiftUI modifiers which currently block the search: - -* `navigationBarItems` -* `overlayPreferenceValue` -* `backgroundPreferenceValue` - -While the first two can be unwrapped manually, the last two are notorious for blocking the inspection completely. The workaround is under investigation. - ## Inspectable attributes **ViewInspector** provides access to various parameters held inside Views. diff --git a/readiness.md b/readiness.md index a04ed5f0..1d64712e 100644 --- a/readiness.md +++ b/readiness.md @@ -10,7 +10,6 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:white_check_mark:| Full inspection support with access to the underlying values or callbacks | |:heavy_check_mark:| Not inspectable itself but does not block inspection of the underlying hierarchy | -|:x:| Blocks inspection of the underlying hierarchy | |:technologist:| Pending development (accepting PRs!) | ## View Types @@ -415,8 +414,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| |:heavy_check_mark:| `func onPreferenceChange(K.Type, perform: (K.Value) -> Void) -> some View` | -|:x:| `func backgroundPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | -|:x:| `func overlayPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | +|:white_check_mark:| `func backgroundPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | +|:white_check_mark:| `func overlayPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | ### Setting the Environment Values of a View From ef98a58d060ef90846526d6883f9d087e3832677 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 17:02:41 +0300 Subject: [PATCH 28/54] Log missing views and property wrappers added in iOS 14 and 15 --- readiness.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/readiness.md b/readiness.md index 1d64712e..7c45e437 100644 --- a/readiness.md +++ b/readiness.md @@ -20,11 +20,15 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| Alert | `title view`, `message view`, `actions view`, `primaryButton`, `secondaryButton`, `dismiss()` | |:white_check_mark:| AngularGradient | `gradient: Gradient`, `center: UnitPoint`, `startAngle: Angle`, `endAngle: Angle` | |:white_check_mark:| AnyView | `contained view` | +|:technologist:| ArtworkImage | | +|:technologist:| AsyncImage | | |:white_check_mark:| Button | `label view`, `role: ButtonRole?`, `tap()` | |:white_check_mark:| ButtonStyleConfiguration.Label | | |:technologist:| CameraView | | +|:technologist:| Canvas | | |:white_check_mark:| Color | `value: Color`, `rgba: (Float, Float, Float, Float)`, `name: String` | |:white_check_mark:| ColorPicker | `label view`, `select(color: Color)` | +|:technologist:| ControlGroup | | |:white_check_mark:| ConditionalContent | `contained view` | |:white_check_mark:| ConfirmationDialog | `title view`, `message view`, `actions view`, `titleVisibility: Visibility`, `dismiss()` | |:white_check_mark:| Custom View | `actualView: CustomView`, `viewBuilder container` | @@ -42,6 +46,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| Font (*) | `size: CGFloat`, `isFixedSize: Bool`, `name: String`, `weight: Font.Weight`, `design: Font.Design`, `style: Font.TextStyle` | |:white_check_mark:| ForEach | `contained view`, `callOnDelete`, `callOnMove`, `callOnInsert` | |:white_check_mark:| Form | `contained view` | +|:technologist:| Gauge | | |:white_check_mark:| FullScreenCover | `dismiss()` | |:white_check_mark:| GeometryReader | `contained view` | |:white_check_mark:| Group | `contained view` | @@ -60,6 +65,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| LinearGradient | `gradient: Gradient`, `startPoint: UnitPoint`, `endPoint: UnitPoint` | |:white_check_mark:| Link | `label view`, `url: URL` | |:white_check_mark:| List | `contained view` | +|:technologist:| LocationButton | | |:white_check_mark:| Map | `(set)coordinateRegion: MKCoordinateRegion`, `(set)userTrackingMode: MapUserTrackingMode`, `(set)mapRect: MKMapRect`, `interactionModes: MapInteractionModes`, `showsUserLocation: Bool` | |:white_check_mark:| MapAnnotation | `coordinate: CLLocationCoordinate2D`, `viewType: MapAnnotation.Type`, (*)`anchorPoint: CGPoint`, (*)`tintColor: Color?`, (*)`contained view` | |:white_check_mark:| Menu | `contained view`, `label view` | @@ -69,8 +75,10 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| ModifiedContent | `contained view` | |:white_check_mark:| NavigationLink | `contained view`, `label view`, `isActive: Bool`, `activate()`, `deactivate()` | |:white_check_mark:| NavigationView | `contained view` | -|:white_check_mark:| OptionalContent | `contained view` | +|:technologist:| NowPlayingView | | +|:white_check_mark:| Optional | `contained view` | |:white_check_mark:| OutlineGroup | `leaf view`, `source data` | +|:technologist:| OutlineSubgroupChildren | | |:white_check_mark:| PasteButton | `supportedTypes: [String]`| |:white_check_mark:| Picker | `contained view`, `label view`, `select(value: Hashable)` | |:white_check_mark:| Popover | `contained view`, `attachmentAnchor: PopoverAttachmentAnchor`, `arrowEdge: Edge`, `dismiss()` | @@ -94,9 +102,11 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| Stepper | `label view`, `increment()`, `decrement()`, `callOnEditingChanged()` | |:heavy_check_mark:| SubscriptionView | | |:white_check_mark:| TabView | `contained view` | +|:technologist:| Table | | |:white_check_mark:| Text | `string(locale: Locale) -> String`, `attributes: TextAttributes`, `images: [Image]` | |:white_check_mark:| TextEditor | `input: String`, `setInput(_: String)` | |:white_check_mark:| TextField | `label view`, `callOnEditingChanged()`, `callOnCommit()`, `input: String`, `setInput(_: String)` | +|:technologist:| TimelineView | | |:white_check_mark:| Toggle | `label view`, `tap()`, `isOn: Bool` | |:white_check_mark:| ToggleStyleConfiguration.Label | | |:technologist:| ToolbarItem | | @@ -104,6 +114,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| TupleView | | |:white_check_mark:| VSplitView | `contained view` | |:white_check_mark:| VStack | `contained view`, `alignment: VerticalAlignment`, `spacing: CGFloat?` | +|:technologist:| VideoPlayer | | |:white_check_mark:| ZStack | `contained view` | (*) The following attributes are available directly for the `Font` and `Image` SwiftUI types, as opposed to the attributes available for wrapper views extracted from the hierarchy. In case you obtained an image view from the hierarchy using `image()` call, you'd need to additionally call `actualImage: Image` to get the genuine `Image` structure. @@ -112,11 +123,13 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| +|:technologist:| `@AccessibilityFocusState` | |:technologist:| `@AppStorage` | |:white_check_mark:| `@Binding` | |:white_check_mark:| `@Environment` | |:white_check_mark:| `@EnvironmentObject` | |:technologist:| `@FetchRequest` | +|:technologist:| `@FocusState` | |:technologist:| `@FocusedBinding` | |:technologist:| `@FocusedValue` | |:white_check_mark:| `@GestureState` | @@ -124,6 +137,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `@ObservedObject` | |:technologist:| `@ScaledMetric` | |:technologist:| `@SceneStorage` | +|:technologist:| `@SectionedFetchRequest` | |:white_check_mark:| `@State` | |:technologist:| `@StateObject` | |:technologist:| `@UIApplicationDelegateAdaptor` | From baf043cceccc4818c0a85337cc8fb856e9ff28ce Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Wed, 22 Dec 2021 22:22:18 +0300 Subject: [PATCH 29/54] Log missing view modifiers added in iOS 14 and 15 --- readiness.md | 133 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 23 deletions(-) diff --git a/readiness.md b/readiness.md index 7c45e437..4ce412c6 100644 --- a/readiness.md +++ b/readiness.md @@ -4,6 +4,8 @@ This document reflects the current status of the [ViewInspector](https://github. Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) for helping me prioritize the work on a specific APIs. +#### Please open a PR if any `View` or `Modifier` is not listed! + ### Denotations | Status | Meaning | @@ -142,6 +144,17 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:technologist:| `@StateObject` | |:technologist:| `@UIApplicationDelegateAdaptor` | +## Special UI entities + +| Status | Modifier | +|:---:|---| +|:technologist:| Widget | +|:technologist:| Scene | +|:technologist:| DocumentGroup | +|:technologist:| Settings | +|:technologist:| WKNotificationScene | +|:technologist:| WindowGroup | + ## Gestures | Status | Modifier | @@ -179,6 +192,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func offset(x: CGFloat, y: CGFloat) -> some View` | |:white_check_mark:| `func edgesIgnoringSafeArea(Edge.Set) -> some View` | |:white_check_mark:| `func coordinateSpace(name: T) -> some View` | +|:technologist:| `func ignoresSafeArea(SafeAreaRegions, edges: Edge.Set) -> some View` | +|:technologist:| `func safeAreaInset(edge: VerticalEdge, alignment: HorizontalAlignment, spacing: CGFloat?, content: () -> V) -> some View` | ### Aligning Views @@ -194,15 +209,19 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func padding(CGFloat) -> some View` | |:white_check_mark:| `func padding(EdgeInsets) -> some View` | |:white_check_mark:| `func padding(Edge.Set, CGFloat?) -> some View` | -|:technologist:| `func ignoresSafeArea(SafeAreaRegions, edges: Edge.Set) -> some View` | +|:technologist:| `func scenePadding(_ edges: Edge.Set) -> some View` | ### Layering Views | Status | Modifier | |:---:|---| |:white_check_mark:| `func overlay(Overlay, alignment: Alignment) -> some View` | +|:technologist:| `func overlay(_ style: S, ignoresSafeAreaEdges edges: Edge.Set) -> some View` | |:white_check_mark:| `func background(Background, alignment: Alignment) -> some View` | +|:technologist:| `func background(_ style: S, ignoresSafeAreaEdges: Edge.Set) -> some View` | |:white_check_mark:| `func zIndex(Double) -> some View` | +|:technologist:| `func badge(...) -> some View` | +|:white_check_mark:| `func border(S, width: CGFloat) -> some View` | ### Masking and Clipping Views @@ -212,6 +231,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func clipShape(S, style: FillStyle) -> some View` | |:white_check_mark:| `func cornerRadius(CGFloat, antialiased: Bool) -> some View` | |:white_check_mark:| `func mask(Mask) -> some View` | +|:technologist:| `func mask(alignment: Alignment = .center, mask: () -> Mask) -> some View` | +|:technologist:| `func containerShape(_ shape: T) -> some View` | ### Scaling Views @@ -234,6 +255,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func rotation3DEffect(Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint, anchorZ: CGFloat, perspective: CGFloat) -> some View` | |:white_check_mark:| `func projectionEffect(ProjectionTransform) -> some View` | |:white_check_mark:| `func transformEffect(CGAffineTransform) -> some View` | +|:technologist:| `func dynamicTypeSize(...) -> some View` | ### Applying Graphical Effects to a View @@ -265,7 +287,10 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:heavy_check_mark:| `func animation(Animation?) -> some View` | |:heavy_check_mark:| `func animation(Animation?, value: V) -> some View` | +|:technologist:| `func animation(_ animation: Animation?) -> some ViewModifier` | |:white_check_mark:| `func transition(AnyTransition) -> some View` | +|:white_check_mark:| `func transaction((inout Transaction) -> Void) -> some View` | +|:technologist:| `func transaction((inout Transaction) -> Void) -> some ViewModifier` | |:technologist:| `func matchedGeometryEffect(id: ID, in: Namespace.ID, properties: MatchedGeometryProperties, anchor: UnitPoint, isSource: Bool) -> some View` | ### Text modifiers @@ -292,7 +317,6 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func gesture(T, including: GestureMask) -> some View` | |:white_check_mark:| `func highPriorityGesture(T, including: GestureMask) -> some View` | |:white_check_mark:| `func simultaneousGesture(T, including: GestureMask) -> some View` | -|:white_check_mark:| `func transaction((inout Transaction) -> Void) -> some View` | ### Handling Application Life Cycle Events @@ -331,6 +355,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:heavy_check_mark:| `func onReceive

(P, perform: (P.Output) -> Void) -> some View` | |:white_check_mark:| `func onChange(of: V, perform: (V) -> Void) -> some View` | +|:technologist:| `func task(...) -> some View` | ### Handling Keyboard Shortcuts @@ -343,12 +368,16 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| +|:technologist:| `func hoverEffect(HoverEffect) -> some View` | |:heavy_check_mark:| `func onHover(perform: (Bool) -> Void) -> some View` | -|:white_check_mark:| `func focusable(Bool, onFocusChange: (Bool) -> Void) -> some View` | |:technologist:| `func focusedValue(WritableKeyPath, Value) -> some View` | |:technologist:| `func prefersDefaultFocus(Bool, in: Namespace.ID) -> some View` | |:technologist:| `func focusScope(Namespace.ID) -> some View` | -|:technologist:| `func hoverEffect(HoverEffect) -> some View` | +|:technologist:| `func focusSection() -> some View` | +|:white_check_mark:| `func focusable(Bool, onFocusChange: (Bool) -> Void) -> some View` | +|:technologist:| `func focusable(_ isFocusable: Bool) -> some View` | +|:technologist:| `func focused(...) -> some View` | +|:technologist:| `func focusedSceneValue(_ keyPath: WritableKeyPath, _ value: T) -> some View` | ### Supporting Drag and Drop in Views @@ -369,6 +398,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:white_check_mark:| `func allowsHitTesting(Bool) -> some View` | |:white_check_mark:| `func contentShape(S, eoFill: Bool) -> some View` | +|:technologist:| `func contentShape(_ kind: ContentShapeKinds, _ shape: S, eoFill: Bool) -> some View` | ### Presenting system popup views @@ -378,22 +408,26 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func actionSheet(item: Binding, content: (T) -> ActionSheet) -> some View` | |:white_check_mark:| `func sheet(isPresented: Binding, onDismiss: (() -> Void)?, content: () -> Content) -> some View` | |:white_check_mark:| `func sheet(item: Binding, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View` | -|: white_check_mark:| `func fullScreenCover(isPresented: Binding, onDismiss: (() -> Void)?, content: () -> Content) -> some View` | -|: white_check_mark:| `func fullScreenCover(item: Binding, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View` | +|:white_check_mark:| `func fullScreenCover(isPresented: Binding, onDismiss: (() -> Void)?, content: () -> Content) -> some View` | +|:white_check_mark:| `func fullScreenCover(item: Binding, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View` | |:white_check_mark:| `func alert(isPresented: Binding, content: () -> Alert) -> some View` | |:white_check_mark:| `func alert(item: Binding, content: (Item) -> Alert) -> some View` | |:white_check_mark:| `func popover(isPresented: Binding, attachmentAnchor: PopoverAttachmentAnchor, arrowEdge: Edge, content: () -> Content) -> some View` | |:white_check_mark:| `func popover(item: Binding, attachmentAnchor: PopoverAttachmentAnchor, arrowEdge: Edge, content: (Item) -> Content) -> some View` | |:white_check_mark:| `func confirmationDialog(_ title: S, isPresented: Binding, titleVisibility: Visibility, @ViewBuilder actions: () -> A, @ViewBuilder message: () -> M) -> some View` | +|:technologist:| `func interactiveDismissDisabled(_ isDisabled: Bool) -> some View` | ### APIs from other Frameworks | Status | Modifier | |:---:|---| |:technologist:| `func appStoreOverlay(isPresented: Binding, configuration: @escaping () -> SKOverlay.Configuration) -> some View` | +|:technologist:| `func manageSubscriptionsSheet(isPresented: Binding) -> some View` | +|:technologist:| `func refundRequestSheet(for transactionID: UInt64, isPresented: Binding, onDismiss: ...) -> some View` | |:technologist:| `func quickLookPreview(_ selection: Binding, in items: Items) -> some View` | |:technologist:| `func quickLookPreview(_ item: Binding) -> some View` | |:technologist:| `func signInWithAppleButtonStyle(_ style: SignInWithAppleButton.Style) -> some View` | +|:technologist:| `func musicSubscriptionOffer(isPresented: Binding, options: ..., onLoadCompletion: ...) -> some View` | ### Presenting File Management Interfaces @@ -407,6 +441,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:technologist:| `func fileImporter(isPresented: Binding, allowedContentTypes: [UTType], onCompletion: (Result) -> Void) -> some View` | |:technologist:| `func fileMover(isPresented: Binding, file: URL?, onCompletion: (Result) -> Void) -> some View` | |:technologist:| `func fileMover(isPresented: Binding, files: C, onCompletion: (Result<[URL], Error>) -> Void) -> some View` | +|:technologist:| `func exportsItemProviders(_ contentTypes: [UTType], onExport: @escaping () -> [NSItemProvider]) -> some View` | +|:technologist:| `func importsItemProviders(_ contentTypes: [UTType], onImport: @escaping ([NSItemProvider]) -> Bool) -> some View` | ### Choosing the Default Storage @@ -439,25 +475,17 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:heavy_check_mark:| `func environmentObject(B) -> some View` | |:heavy_check_mark:| `func transformEnvironment(WritableKeyPath, transform: (inout V) -> Void) -> some View` | -### Setting the Border of a View - -| Status | Modifier | -|:---:|---| -|:white_check_mark:| `func border(S, width: CGFloat) -> some View` | - ### Setting View Colors | Status | Modifier | |:---:|---| |:white_check_mark:| `func foregroundColor(Color?) -> some View` | +|:technologist:| `func foregroundStyle(...) -> some View` | |:white_check_mark:| `func accentColor(Color?) -> some View` | |:white_check_mark:| `func tint(Color?) -> some View` | |:white_check_mark:| `func listItemTint(Color?) -> some View` | - -### Adopting View Color Schemes - -| Status | Modifier | -|:---:|---| +|:technologist:| `func symbolRenderingMode(_ mode: SymbolRenderingMode?) -> some View` | +|:technologist:| `func symbolVariant(_ variant: SymbolVariants) -> some View` | |:white_check_mark:| `func colorScheme(ColorScheme) -> some View` | |:white_check_mark:| `func preferredColorScheme(ColorScheme?) -> some View` | @@ -474,12 +502,17 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func truncationMode(Text.TruncationMode) -> some View` | |:white_check_mark:| `func allowsTightening(Bool) -> some View` | |:white_check_mark:| `func textContentType(UITextContentType?) -> some View` | -|:technologist:| `func textContentType(NSTextContentType?) -> some View` | -|:technologist:| `func textContentType(WKTextContentType?) -> some View` | +|:technologist:| `func textContentType(UITextContentType?) -> some View` | |:technologist:| `func textCase(Text.Case?) -> some View` | |:white_check_mark:| `func flipsForRightToLeftLayoutDirection(Bool) -> some View` | |:white_check_mark:| `func autocapitalization(UITextAutocapitalizationType) -> some View` | |:white_check_mark:| `func disableAutocorrection(Bool?) -> some View` | +|:technologist:| `func monospacedDigit() -> some View` | +|:technologist:| `func textInputAutocapitalization(_ autocapitalization: TextInputAutocapitalization?) -> some View` | +|:technologist:| `func onSubmit(of triggers: SubmitTriggers, _ action: @escaping (() -> Void)) -> some View` | +|:technologist:| `func submitLabel(_ submitLabel: SubmitLabel) -> some View` | +|:technologist:| `func submitScope(_ isBlocking: Bool) -> some View` | +|:technologist:| `func textSelection(_ selectability: S) -> some View` | ### Redacting Content @@ -487,6 +520,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:technologist:| `func redacted(reason: RedactionReasons) -> some View` | |:technologist:| `func unredacted() -> some View` | +|:technologist:| `func privacySensitive(_ sensitive: Bool) -> some View` | ### Configuring Control Attributes @@ -516,6 +550,10 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func tabViewStyle(S) -> some View` | |:white_check_mark:| `func textFieldStyle(S) -> some View` | |:white_check_mark:| `func toggleStyle(S) -> some View` | +|:technologist:| `func controlGroupStyle(S) -> some View` | +|:technologist:| `func gaugeStyle(S) -> some View` | +|:technologist:| `func signInWithAppleButtonStyle(S) -> some View` | +|:technologist:| `func buttonBorderShape(_ shape: ButtonBorderShape) -> some View` | ### Configuring a List View @@ -527,6 +565,15 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:technologist:| `func listItemTint(ListItemTint?) -> some View` | |:technologist:| `func listItemTint(Color?) -> some View` | |:white_check_mark:| `func tag(V) -> some View` | +|:technologist:| `func swipeActions(edge: HorizontalEdge, allowsFullSwipe: Bool, content: () -> T) -> some View` | +|:technologist:| `func listRowSeparator(_ visibility: Visibility, edges: VerticalEdge.Set) -> some View` | +|:technologist:| `func listRowSeparatorTint(_ color: Color?, edges: VerticalEdge.Set) -> some View` | +|:technologist:| `func listSectionSeparator(_ visibility: Visibility, edges: VerticalEdge.Set) -> some View` | +|:technologist:| `func listSectionSeparatorTint(_ color: Color?, edges: VerticalEdge.Set) -> some View` | +|:technologist:| `func headerProminence(_ prominence: Prominence) -> some View` | +|:technologist:| `func refreshable(action: @Sendable () async -> Void) -> some View` | +|:technologist:| `func searchable(text: Binding, placement: SearchFieldPlacement, prompt: LocalizedStringKey) -> some View` | +|:technologist:| `func searchCompletion(_ completion: String) -> some View` | ### Configuring the Navigation Title @@ -567,9 +614,9 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|: white_check_mark:| `func toolbar(content: () -> Content) -> some View` | -|: white_check_mark:| `func toolbar(content: () -> Content) -> some View` | -|: white_check_mark:| `func toolbar(id: String, content: () -> Content) -> some View` | +|:white_check_mark:| `func toolbar(content: () -> Content) -> some View` | +|:white_check_mark:| `func toolbar(content: () -> Content) -> some View` | +|:white_check_mark:| `func toolbar(id: String, content: () -> Content) -> some View` | ### Configuring Context Menu Views @@ -577,6 +624,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:---:|---| |:heavy_check_mark:| `func contextMenu(ContextMenu?) -> some View` | |:heavy_check_mark:| `func contextMenu(menuItems: () -> MenuItems) -> some View` | +|:technologist:| `func menuIndicator(_ visibility: Visibility) -> some View` | ### Configuring Touch Bar Views @@ -618,26 +666,54 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:white_check_mark:| `func accessibilityActivationPoint(CGPoint) -> ModifiedContent` | |:white_check_mark:| `func accessibilityActivationPoint(UnitPoint) -> ModifiedContent` | |:white_check_mark:| `func accessibilityAction(AccessibilityActionKind, () -> Void) -> ModifiedContent` | +|:technologist:| `func accessibilityAction

(P, perform: (P.Output) -> Void) -> some View` | +|:white_check_mark:| `func onReceive

(P, perform: (P.Output) -> Void) -> some View` | |:white_check_mark:| `func onChange(of: V, perform: (V) -> Void) -> some View` | |:technologist:| `func task(...) -> some View` | @@ -369,7 +367,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| |:technologist:| `func hoverEffect(HoverEffect) -> some View` | -|:heavy_check_mark:| `func onHover(perform: (Bool) -> Void) -> some View` | +|:technologist:| `func onHover(perform: (Bool) -> Void) -> some View` | |:technologist:| `func focusedValue(WritableKeyPath, Value) -> some View` | |:technologist:| `func prefersDefaultFocus(Bool, in: Namespace.ID) -> some View` | |:technologist:| `func focusScope(Namespace.ID) -> some View` | @@ -383,14 +381,14 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func onDrag(() -> NSItemProvider) -> some View` | +|:technologist:| `func onDrag(() -> NSItemProvider) -> some View` | |:technologist:| `func onDrop(of: [UTType], delegate: DropDelegate) -> some View` | |:technologist:| `func onDrop(of: [UTType], isTargeted: Binding?, perform: ([NSItemProvider]) -> Bool) -> some View` | |:technologist:| `func onDrop(of: [UTType], isTargeted: Binding?, perform: ([NSItemProvider], CGPoint) -> Bool) -> some View` | |:heavy_check_mark:| `func onDrop(of: [String], delegate: DropDelegate) -> some View` | |:heavy_check_mark:| `func onDrop(of: [String], isTargeted: Binding?, perform: ([NSItemProvider], CGPoint) -> Bool) -> some View` | |:heavy_check_mark:| `func onDrop(of: [String], isTargeted: Binding?, perform: ([NSItemProvider]) -> Bool) -> some View` | -|:heavy_check_mark:| `func itemProvider(Optional<() -> NSItemProvider?>) -> some View` | +|:technologist:| `func itemProvider(Optional<() -> NSItemProvider?>) -> some View` | ### Configuring a View for Hit Testing @@ -454,16 +452,16 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func preference(key: K.Type, value: K.Value) -> some View` | -|:heavy_check_mark:| `func transformPreference(K.Type, (inout K.Value) -> Void) -> some View` | -|:heavy_check_mark:| `func anchorPreference(key: K.Type, value: Anchor.Source, transform: (Anchor) -> K.Value) -> some View` | -|:heavy_check_mark:| `func transformAnchorPreference(key: K.Type, value: Anchor.Source, transform: (inout K.Value, Anchor) -> Void) -> some View` | +|:technologist:| `func preference(key: K.Type, value: K.Value) -> some View` | +|:technologist:| `func transformPreference(K.Type, (inout K.Value) -> Void) -> some View` | +|:technologist:| `func anchorPreference(key: K.Type, value: Anchor.Source, transform: (Anchor) -> K.Value) -> some View` | +|:technologist:| `func transformAnchorPreference(key: K.Type, value: Anchor.Source, transform: (inout K.Value, Anchor) -> Void) -> some View` | ### Responding to View Preferences | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func onPreferenceChange(K.Type, perform: (K.Value) -> Void) -> some View` | +|:technologist:| `func onPreferenceChange(K.Type, perform: (K.Value) -> Void) -> some View` | |:white_check_mark:| `func backgroundPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | |:white_check_mark:| `func overlayPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | @@ -471,9 +469,9 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func environment(WritableKeyPath, V) -> some View` | -|:heavy_check_mark:| `func environmentObject(B) -> some View` | -|:heavy_check_mark:| `func transformEnvironment(WritableKeyPath, transform: (inout V) -> Void) -> some View` | +|:white_check_mark:| `func environmentObject(B) -> some View` | +|:technologist:| `func environment(WritableKeyPath, V) -> some View` | +|:technologist:| `func transformEnvironment(WritableKeyPath, transform: (inout V) -> Void) -> some View` | ### Setting View Colors @@ -597,8 +595,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) |:heavy_check_mark:| `func navigationBarTitle(S) -> some View` | |:heavy_check_mark:| `func navigationBarTitle(LocalizedStringKey, displayMode: NavigationBarItem.TitleDisplayMode) -> some View` | |:technologist:| `func navigationBarTitleDisplayMode(NavigationBarItem.TitleDisplayMode) -> some View` | -|:heavy_check_mark:| `func navigationBarHidden(Bool) -> some View` | -|:heavy_check_mark:| `func statusBar(hidden: Bool) -> some View` | +|:technologist:| `func navigationBarHidden(Bool) -> some View` | +|:technologist:| `func statusBar(hidden: Bool) -> some View` | ### Configuring Navigation and Tab Bar Item Views @@ -622,8 +620,8 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func contextMenu(ContextMenu?) -> some View` | -|:heavy_check_mark:| `func contextMenu(menuItems: () -> MenuItems) -> some View` | +|:technologist:| `func contextMenu(ContextMenu?) -> some View` | +|:technologist:| `func contextMenu(menuItems: () -> MenuItems) -> some View` | |:technologist:| `func menuIndicator(_ visibility: Visibility) -> some View` | ### Configuring Touch Bar Views @@ -703,7 +701,7 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func accessibilityElement(children: AccessibilityChildBehavior) -> some View` | +|:technologist:| `func accessibilityElement(children: AccessibilityChildBehavior) -> some View` | |:technologist:| `func accessibilityChildren(children: () -> V) -> some View` | |:technologist:| `func accessibilityInputLabels([LocalizedStringKey]) -> ModifiedContent` | |:technologist:| `func accessibilityInputLabels([S]) -> ModifiedContent` | @@ -727,18 +725,18 @@ Visit [this discussion](https://github.com/nalexn/ViewInspector/discussions/60) | Status | Modifier | |:---:|---| -|:white_check_mark:| `func accessibility(label: Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(value: Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(hidden: Bool) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(identifier: String) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(selectionIdentifier: AnyHashable) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(hint: Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(activationPoint: UnitPoint) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(activationPoint: CGPoint) -> ModifiedContent` | -|:technologist:| `func accessibility(inputLabels: [Text]) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(label: Text) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(value: Text) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(hidden: Bool) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(identifier: String) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(selectionIdentifier: AnyHashable) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(hint: Text) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(activationPoint: UnitPoint) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(activationPoint: CGPoint) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(inputLabels: [Text]) -> ModifiedContent` | |:heavy_check_mark:| `func accessibility(addTraits: AccessibilityTraits) -> ModifiedContent` | |:heavy_check_mark:| `func accessibility(removeTraits: AccessibilityTraits) -> ModifiedContent` | -|:white_check_mark:| `func accessibility(sortPriority: Double) -> ModifiedContent` | +|:heavy_check_mark:| `func accessibility(sortPriority: Double) -> ModifiedContent` | ### Configuring View Previews From 4cde909bbfab85f03d2360b1401cc8e8af30b996 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Thu, 23 Dec 2021 13:45:00 +0300 Subject: [PATCH 31/54] Add support for ContainerRelativeShape --- Sources/ViewInspector/SwiftUI/Shape.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ViewInspector/SwiftUI/Shape.swift b/Sources/ViewInspector/SwiftUI/Shape.swift index 059fc330..0072a1a9 100644 --- a/Sources/ViewInspector/SwiftUI/Shape.swift +++ b/Sources/ViewInspector/SwiftUI/Shape.swift @@ -174,6 +174,9 @@ extension RotatedShape: InspectableShape { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension ScaledShape: InspectableShape { } +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension ContainerRelativeShape: InspectableShape { } + @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension _SizedShape: InspectableShape { } From 93a34b6daf0d2a99882e53fe303d006f100273ef Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Thu, 23 Dec 2021 13:51:07 +0300 Subject: [PATCH 32/54] Minor tweaks --- Sources/ViewInspector/SwiftUI/EnvironmentReaderView.swift | 4 ---- Sources/ViewInspector/ViewSearchIndex.swift | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ViewInspector/SwiftUI/EnvironmentReaderView.swift b/Sources/ViewInspector/SwiftUI/EnvironmentReaderView.swift index b8832472..923191e2 100644 --- a/Sources/ViewInspector/SwiftUI/EnvironmentReaderView.swift +++ b/Sources/ViewInspector/SwiftUI/EnvironmentReaderView.swift @@ -22,14 +22,10 @@ extension ViewType.EnvironmentReaderView: SingleViewContent { @available(watchOS, unavailable) public extension InspectableView where View: SingleViewContent { - @available(iOS, deprecated: 100000.0, message: "Please use `toolbar()` for inspecting `navigationBarItems`") - @available(tvOS, deprecated: 100000.0, message: "Please use `toolbar()` for inspecting `navigationBarItems`") func navigationBarItems() throws -> InspectableView { return try navigationBarItems(AnyView.self) } - @available(iOS, deprecated: 100000.0, message: "Please use `toolbar()` for inspecting `navigationBarItems`") - @available(tvOS, deprecated: 100000.0, message: "Please use `toolbar()` for inspecting `navigationBarItems`") func navigationBarItems(_ viewType: V.Type) throws -> InspectableView where V: SwiftUI.View { return try navigationBarItems(viewType: viewType, content: try child()) diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index 8e175b61..fd829229 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -269,7 +269,8 @@ internal extension ViewType.Overlay.API { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewSearch { - static private(set) var modifierIdentities: [ModifierIdentity] = ViewType.Overlay.API.viewSearchModifierIdentities + [ + static private(set) var modifierIdentities: [ModifierIdentity] = ViewType.Overlay.API.viewSearchModifierIdentities + + [ .init(name: ViewType.Toolbar.typePrefix, builder: { parent, index in try parent.content.toolbar(parent: parent, index: index) }), From c43c17272a864ffa512154b1b8e21fd749894265 Mon Sep 17 00:00:00 2001 From: Alexey Naumov Date: Thu, 23 Dec 2021 15:02:23 +0300 Subject: [PATCH 33/54] Groom the readiness list --- readiness.md | 364 +++++++++++++++++---------------------------------- 1 file changed, 117 insertions(+), 247 deletions(-) diff --git a/readiness.md b/readiness.md index 1f5fe3f7..692224e9 100644 --- a/readiness.md +++ b/readiness.md @@ -109,7 +109,7 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| TimelineView | | |:white_check_mark:| Toggle | `label view`, `tap()`, `isOn: Bool` | |:white_check_mark:| ToggleStyleConfiguration.Label | | -|:technologist:| ToolbarItem | | +|:white_check_mark:| ToolbarItem | | |:white_check_mark:| TouchBar | `contained view`, `touchBarID: String` | |:white_check_mark:| TupleView | | |:white_check_mark:| VSplitView | `contained view` | @@ -142,17 +142,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| `@StateObject` | |:technologist:| `@UIApplicationDelegateAdaptor` | -## Special UI entities - -| Status | Modifier | -|:---:|---| -|:technologist:| Widget | -|:technologist:| Scene | -|:technologist:| DocumentGroup | -|:technologist:| Settings | -|:technologist:| WKNotificationScene | -|:technologist:| WindowGroup | - ## Gestures | Status | Modifier | @@ -170,43 +159,42 @@ This document reflects the current status of the [ViewInspector](https://github. ## View Modifiers -### Sizing a View +### Custom View Modifiers | Status | Modifier | |:---:|---| -|:white_check_mark:| `func frame(width: CGFloat?, height: CGFloat?, alignment: Alignment) -> some View` | -|:white_check_mark:| `func frame(minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, idealHeight: CGFloat?, maxHeight: CGFloat?, alignment: Alignment) -> some View` | -|:white_check_mark:| `func fixedSize() -> some View` | -|:white_check_mark:| `func fixedSize(horizontal: Bool, vertical: Bool) -> some View` | -|:white_check_mark:| `func layoutPriority(Double) -> some View` | +|:white_check_mark:| `func modifier(T) -> ModifiedContent` | +|:technologist:| `func concat(_ modifier: T) -> ModifiedContent` | -### Positioning a View +### Inspecting Views | Status | Modifier | |:---:|---| -|:white_check_mark:| `func position(CGPoint) -> some View` | -|:white_check_mark:| `func position(x: CGFloat, y: CGFloat) -> some View` | -|:white_check_mark:| `func offset(CGSize) -> some View` | -|:white_check_mark:| `func offset(x: CGFloat, y: CGFloat) -> some View` | -|:white_check_mark:| `func edgesIgnoringSafeArea(Edge.Set) -> some View` | -|:white_check_mark:| `func coordinateSpace(name: T) -> some View` | -|:technologist:| `func ignoresSafeArea(SafeAreaRegions, edges: Edge.Set) -> some View` | -|:technologist:| `func safeAreaInset(edge: VerticalEdge, alignment: HorizontalAlignment, spacing: CGFloat?, content: () -> V) -> some View` | +|:white_check_mark:| `func id(ID) -> some View` | +|:white_check_mark:| `func equatable() -> EquatableView` | -### Aligning Views +### Hiding and Disabling Views | Status | Modifier | |:---:|---| -|:technologist:| `func alignmentGuide(HorizontalAlignment, computeValue: (ViewDimensions) -> CGFloat) -> some View` | -|:technologist:| `func alignmentGuide(VerticalAlignment, computeValue: (ViewDimensions) -> CGFloat) -> some View` | +|:white_check_mark:| `func hidden() -> some View` | +|:white_check_mark:| `func disabled(Bool) -> some View` | -### Adjusting the Padding of a View +### Sizing and Positioning a View | Status | Modifier | |:---:|---| -|:white_check_mark:| `func padding(CGFloat) -> some View` | -|:white_check_mark:| `func padding(EdgeInsets) -> some View` | -|:white_check_mark:| `func padding(Edge.Set, CGFloat?) -> some View` | +|:white_check_mark:| `func frame(...) -> some View` | +|:white_check_mark:| `func fixedSize(...) -> some View` | +|:white_check_mark:| `func layoutPriority(Double) -> some View` | +|:white_check_mark:| `func position(...) -> some View` | +|:white_check_mark:| `func offset(...) -> some View` | +|:white_check_mark:| `func edgesIgnoringSafeArea(Edge.Set) -> some View` | +|:white_check_mark:| `func coordinateSpace(name: T) -> some View` | +|:technologist:| `func ignoresSafeArea(SafeAreaRegions, edges: Edge.Set) -> some View` | +|:technologist:| `func safeAreaInset(edge: VerticalEdge, alignment: HorizontalAlignment, spacing: CGFloat?, content: () -> V) -> some View` | +|:technologist:| `func alignmentGuide(...) -> some View` | +|:white_check_mark:| `func padding(...) -> some View` | |:technologist:| `func scenePadding(_ edges: Edge.Set) -> some View` | ### Layering Views @@ -238,11 +226,8 @@ This document reflects the current status of the [ViewInspector](https://github. |:---:|---| |:white_check_mark:| `func scaledToFill() -> some View` | |:white_check_mark:| `func scaledToFit() -> some View` | -|:white_check_mark:| `func scaleEffect(CGFloat, anchor: UnitPoint) -> some View` | -|:white_check_mark:| `func scaleEffect(CGSize, anchor: UnitPoint) -> some View` | -|:white_check_mark:| `func scaleEffect(x: CGFloat, y: CGFloat, anchor: UnitPoint) -> some View` | -|:white_check_mark:| `func aspectRatio(CGFloat?, contentMode: ContentMode) -> some View` | -|:white_check_mark:| `func aspectRatio(CGSize, contentMode: ContentMode) -> some View` | +|:white_check_mark:| `func scaleEffect(...) -> some View` | +|:white_check_mark:| `func aspectRatio(...) -> some View` | |:white_check_mark:| `func imageScale(Image.Scale) -> some View` | ### Rotating and Transforming Views @@ -250,7 +235,7 @@ This document reflects the current status of the [ViewInspector](https://github. | Status | Modifier | |:---:|---| |:white_check_mark:| `func rotationEffect(Angle, anchor: UnitPoint) -> some View` | -|:white_check_mark:| `func rotation3DEffect(Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint, anchorZ: CGFloat, perspective: CGFloat) -> some View` | +|:white_check_mark:| `func rotation3DEffect(...) -> some View` | |:white_check_mark:| `func projectionEffect(ProjectionTransform) -> some View` | |:white_check_mark:| `func transformEffect(CGAffineTransform) -> some View` | |:technologist:| `func dynamicTypeSize(...) -> some View` | @@ -291,21 +276,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| `func transaction((inout Transaction) -> Void) -> some ViewModifier` | |:technologist:| `func matchedGeometryEffect(id: ID, in: Namespace.ID, properties: MatchedGeometryProperties, anchor: UnitPoint, isSource: Bool) -> some View` | -### Text modifiers - -| Status | Modifier | -|:---:|---| -|:white_check_mark:| `func foregroundColor(_ color: Color?) -> Text` | -|:white_check_mark:| `func font(_ font: Font?) -> Text` | -|:white_check_mark:| `func fontWeight(_ weight: Font.Weight?) -> Text` | -|:white_check_mark:| `func bold() -> Text` | -|:white_check_mark:| `func italic() -> Text` | -|:white_check_mark:| `func strikethrough(_ active: Bool, color: Color?) -> Text` | -|:white_check_mark:| `func underline(_ active: Bool, color: Color?) -> Text` | -|:white_check_mark:| `func kerning(_ kerning: CGFloat) -> Text` | -|:white_check_mark:| `func tracking(_ tracking: CGFloat) -> Text` | -|:white_check_mark:| `func baselineOffset(_ baselineOffset: CGFloat) -> Text` | - ### Handling View Taps and Gestures | Status | Modifier | @@ -335,15 +305,14 @@ This document reflects the current status of the [ViewInspector](https://github. |:white_check_mark:| `func onDisappear(perform: (() -> Void)?) -> some View` | |:white_check_mark:| `func onCutCommand(perform: (() -> [NSItemProvider])?) -> some View` | |:white_check_mark:| `func onCopyCommand(perform: (() -> [NSItemProvider])?) -> some View` | -|:heavy_check_mark:| `func onPasteCommand(of: [String], perform: ([NSItemProvider]) -> Void) -> some View` | -|:technologist:| `func onPasteCommand(of: [UTType], perform: ([NSItemProvider]) -> Void) -> some View` | -|:technologist:| `func onPasteCommand(of: [UTType], validator: ([NSItemProvider]) -> Payload?, perform: (Payload) -> Void) -> some View` | -|:heavy_check_mark:| `func onPasteCommand(of: [String], validator: ([NSItemProvider]) -> Payload?, perform: (Payload) -> Void) -> some View` | +|:technologist:| `func onPasteCommand(...) -> some View` | |:white_check_mark:| `func onDeleteCommand(perform: (() -> Void)?) -> some View` | |:white_check_mark:| `func onMoveCommand(perform: ((MoveCommandDirection) -> Void)?) -> some View` | |:white_check_mark:| `func onExitCommand(perform: (() -> Void)?) -> some View` | |:technologist:| `func onPlayPauseCommand(perform: (() -> Void)?) -> some View` | |:technologist:| `func onCommand(Selector, perform: (() -> Void)?) -> some View` | +|:technologist:| `func onDrag(() -> NSItemProvider) -> some View` | +|:technologist:| `func onDrop(of: [UTType], ...) -> some View` | |:technologist:| `func deleteDisabled(Bool) -> some View` | |:technologist:| `func moveDisabled(Bool) -> some View` | @@ -355,13 +324,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:white_check_mark:| `func onChange(of: V, perform: (V) -> Void) -> some View` | |:technologist:| `func task(...) -> some View` | -### Handling Keyboard Shortcuts - -| Status | Modifier | -|:---:|---| -|:technologist:| `func keyboardShortcut(KeyboardShortcut) -> some View` | -|:technologist:| `func keyboardShortcut(KeyEquivalent, modifiers: EventModifiers) -> some View` | - ### Handling View Hover and Focus | Status | Modifier | @@ -377,19 +339,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| `func focused(...) -> some View` | |:technologist:| `func focusedSceneValue(_ keyPath: WritableKeyPath, _ value: T) -> some View` | -### Supporting Drag and Drop in Views - -| Status | Modifier | -|:---:|---| -|:technologist:| `func onDrag(() -> NSItemProvider) -> some View` | -|:technologist:| `func onDrop(of: [UTType], delegate: DropDelegate) -> some View` | -|:technologist:| `func onDrop(of: [UTType], isTargeted: Binding?, perform: ([NSItemProvider]) -> Bool) -> some View` | -|:technologist:| `func onDrop(of: [UTType], isTargeted: Binding?, perform: ([NSItemProvider], CGPoint) -> Bool) -> some View` | -|:heavy_check_mark:| `func onDrop(of: [String], delegate: DropDelegate) -> some View` | -|:heavy_check_mark:| `func onDrop(of: [String], isTargeted: Binding?, perform: ([NSItemProvider], CGPoint) -> Bool) -> some View` | -|:heavy_check_mark:| `func onDrop(of: [String], isTargeted: Binding?, perform: ([NSItemProvider]) -> Bool) -> some View` | -|:technologist:| `func itemProvider(Optional<() -> NSItemProvider?>) -> some View` | - ### Configuring a View for Hit Testing | Status | Modifier | @@ -415,39 +364,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:white_check_mark:| `func confirmationDialog(_ title: S, isPresented: Binding, titleVisibility: Visibility, @ViewBuilder actions: () -> A, @ViewBuilder message: () -> M) -> some View` | |:technologist:| `func interactiveDismissDisabled(_ isDisabled: Bool) -> some View` | -### APIs from other Frameworks - -| Status | Modifier | -|:---:|---| -|:technologist:| `func appStoreOverlay(isPresented: Binding, configuration: @escaping () -> SKOverlay.Configuration) -> some View` | -|:technologist:| `func manageSubscriptionsSheet(isPresented: Binding) -> some View` | -|:technologist:| `func refundRequestSheet(for transactionID: UInt64, isPresented: Binding, onDismiss: ...) -> some View` | -|:technologist:| `func quickLookPreview(_ selection: Binding, in items: Items) -> some View` | -|:technologist:| `func quickLookPreview(_ item: Binding) -> some View` | -|:technologist:| `func signInWithAppleButtonStyle(_ style: SignInWithAppleButton.Style) -> some View` | -|:technologist:| `func musicSubscriptionOffer(isPresented: Binding, options: ..., onLoadCompletion: ...) -> some View` | - -### Presenting File Management Interfaces - -| Status | Modifier | -|:---:|---| -|:technologist:| `func fileExporter(isPresented: Binding, document: D?, contentType: UTType, defaultFilename: String?, onCompletion: (Result) -> Void) -> some View` | -|:technologist:| `func fileExporter(isPresented: Binding, document: D?, contentType: UTType, defaultFilename: String?, onCompletion: (Result) -> Void) -> some View` | -|:technologist:| `func fileExporter(isPresented: Binding, documents: C, contentType: UTType, onCompletion: (Result<[URL], Error>) -> Void) -> some View` | -|:technologist:| `func fileExporter(isPresented: Binding, documents: C, contentType: UTType, onCompletion: (Result<[URL], Error>) -> Void) -> some View` | -|:technologist:| `func fileImporter(isPresented: Binding, allowedContentTypes: [UTType], allowsMultipleSelection: Bool, onCompletion: (Result<[URL], Error>) -> Void) -> some View` | -|:technologist:| `func fileImporter(isPresented: Binding, allowedContentTypes: [UTType], onCompletion: (Result) -> Void) -> some View` | -|:technologist:| `func fileMover(isPresented: Binding, file: URL?, onCompletion: (Result) -> Void) -> some View` | -|:technologist:| `func fileMover(isPresented: Binding, files: C, onCompletion: (Result<[URL], Error>) -> Void) -> some View` | -|:technologist:| `func exportsItemProviders(_ contentTypes: [UTType], onExport: @escaping () -> [NSItemProvider]) -> some View` | -|:technologist:| `func importsItemProviders(_ contentTypes: [UTType], onImport: @escaping ([NSItemProvider]) -> Bool) -> some View` | - -### Choosing the Default Storage - -| Status | Modifier | -|:---:|---| -|:technologist:| `func defaultAppStorage(UserDefaults) -> some View` | - ### Setting View Preferences | Status | Modifier | @@ -456,11 +372,6 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| `func transformPreference(K.Type, (inout K.Value) -> Void) -> some View` | |:technologist:| `func anchorPreference(key: K.Type, value: Anchor.Source, transform: (Anchor) -> K.Value) -> some View` | |:technologist:| `func transformAnchorPreference(key: K.Type, value: Anchor.Source, transform: (inout K.Value, Anchor) -> Void) -> some View` | - -### Responding to View Preferences - -| Status | Modifier | -|:---:|---| |:technologist:| `func onPreferenceChange(K.Type, perform: (K.Value) -> Void) -> some View` | |:white_check_mark:| `func backgroundPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | |:white_check_mark:| `func overlayPreferenceValue(Key.Type, (Key.Value) -> T) -> some View` | @@ -487,6 +398,21 @@ This document reflects the current status of the [ViewInspector](https://github. |:white_check_mark:| `func colorScheme(ColorScheme) -> some View` | |:white_check_mark:| `func preferredColorScheme(ColorScheme?) -> some View` | +### Text modifiers + +| Status | Modifier | +|:---:|---| +|:white_check_mark:| `func foregroundColor(_ color: Color?) -> Text` | +|:white_check_mark:| `func font(_ font: Font?) -> Text` | +|:white_check_mark:| `func fontWeight(_ weight: Font.Weight?) -> Text` | +|:white_check_mark:| `func bold() -> Text` | +|:white_check_mark:| `func italic() -> Text` | +|:white_check_mark:| `func strikethrough(_ active: Bool, color: Color?) -> Text` | +|:white_check_mark:| `func underline(_ active: Bool, color: Color?) -> Text` | +|:white_check_mark:| `func kerning(_ kerning: CGFloat) -> Text` | +|:white_check_mark:| `func tracking(_ tracking: CGFloat) -> Text` | +|:white_check_mark:| `func baselineOffset(_ baselineOffset: CGFloat) -> Text` | + ### Adjusting Text in a View | Status | Modifier | @@ -573,194 +499,138 @@ This document reflects the current status of the [ViewInspector](https://github. |:technologist:| `func searchable(text: Binding, placement: SearchFieldPlacement, prompt: LocalizedStringKey) -> some View` | |:technologist:| `func searchCompletion(_ completion: String) -> some View` | -### Configuring the Navigation Title - -| Status | Modifier | -|:---:|---| -|:technologist:| `func navigationTitle(LocalizedStringKey) -> some View` | -|:technologist:| `func navigationTitle(Text) -> some View` | -|:technologist:| `func navigationTitle(S) -> some View` | -|:technologist:| `func navigationTitle(() -> V) -> some View` | -|:technologist:| `func navigationSubtitle(S) -> some View` | -|:technologist:| `func navigationSubtitle(Text) -> some View` | -|:technologist:| `func navigationSubtitle(LocalizedStringKey) -> some View` | - -### Configuring Navigation and Status Bar Views +### Configuring Navigation, Status and Tab Bars | Status | Modifier | |:---:|---| -|:heavy_check_mark:| `func navigationBarTitle(Text) -> some View` | -|:heavy_check_mark:| `func navigationBarTitle(Text, displayMode: NavigationBarItem.TitleDisplayMode) -> some View` | -|:heavy_check_mark:| `func navigationBarTitle(LocalizedStringKey) -> some View` | -|:heavy_check_mark:| `func navigationBarTitle(S) -> some View` | -|:heavy_check_mark:| `func navigationBarTitle(LocalizedStringKey, displayMode: NavigationBarItem.TitleDisplayMode) -> some View` | +|:heavy_check_mark:| `func navigationBarItems(...) -> some View` | +|:heavy_check_mark:| `func navigationBarTitle(...) -> some View` | +|:technologist:| `func navigationTitle(...) -> some View` | +|:technologist:| `func navigationSubtitle(...) -> some View` | |:technologist:| `func navigationBarTitleDisplayMode(NavigationBarItem.TitleDisplayMode) -> some View` | |:technologist:| `func navigationBarHidden(Bool) -> some View` | -|:technologist:| `func statusBar(hidden: Bool) -> some View` | - -### Configuring Navigation and Tab Bar Item Views - -| Status | Modifier | -|:---:|---| -|:heavy_check_mark:| `func navigationBarBackButtonHidden(Bool) -> some View` | -|:heavy_check_mark:| `func navigationBarItems(leading: L) -> some View` | -|:heavy_check_mark:| `func navigationBarItems(leading: L, trailing: T) -> some View` | -|:heavy_check_mark:| `func navigationBarItems(trailing: T) -> some View` | -|:white_check_mark:| `func tabItem(() -> V) -> some View` | - -### Configuring Toolbar Items - -| Status | Modifier | -|:---:|---| +|:technologist:| `func navigationBarBackButtonHidden(Bool) -> some View` | |:white_check_mark:| `func toolbar(content: () -> Content) -> some View` | |:white_check_mark:| `func toolbar(content: () -> Content) -> some View` | |:white_check_mark:| `func toolbar(id: String, content: () -> Content) -> some View` | - -### Configuring Context Menu Views - -| Status | Modifier | -|:---:|---| -|:technologist:| `func contextMenu(ContextMenu?) -> some View` | -|:technologist:| `func contextMenu(menuItems: () -> MenuItems) -> some View` | -|:technologist:| `func menuIndicator(_ visibility: Visibility) -> some View` | +|:white_check_mark:| `func tabItem(() -> V) -> some View` | +|:technologist:| `func statusBar(hidden: Bool) -> some View` | ### Configuring Touch Bar Views | Status | Modifier | |:---:|---| -|:white_check_mark:| `func touchBar(content: () -> Content) -> some View` | -|:white_check_mark:| `func touchBar(TouchBar) -> some View` | +|:white_check_mark:| `func touchBar(...) -> some View` | |:white_check_mark:| `func touchBarItemPrincipal(Bool) -> some View` | |:white_check_mark:| `func touchBarCustomizationLabel(Text) -> some View` | |:white_check_mark:| `func touchBarItemPresence(TouchBarItemPresence) -> some View` | -### Hiding and Disabling Views +### Handling Keyboard Shortcuts | Status | Modifier | |:---:|---| -|:white_check_mark:| `func hidden() -> some View` | -|:white_check_mark:| `func disabled(Bool) -> some View` | +|:technologist:| `func keyboardShortcut(KeyboardShortcut) -> some View` | +|:technologist:| `func keyboardShortcut(KeyEquivalent, modifiers: EventModifiers) -> some View` | -### Customizing Accessibility Labels of a View +### Configuring Context Menu Views | Status | Modifier | |:---:|---| -|:white_check_mark:| `func accessibilityLabel(S) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityLabel(Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityLabel(LocalizedStringKey) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityValue(S) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityValue(LocalizedStringKey) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityValue(Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityHidden(Bool) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityIdentifier(String) -> ModifiedContent` | +|:technologist:| `func contextMenu(...) -> some View` | +|:technologist:| `func menuIndicator(_ visibility: Visibility) -> some View` | +|:technologist:| `func help(...) -> some View` | -### Customizing Accessibility Interactions of a View +### Customizing Accessibility | Status | Modifier | |:---:|---| -|:white_check_mark:| `func accessibilityHint(LocalizedStringKey) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityHint(Text) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityHint(S) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityActivationPoint(CGPoint) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityActivationPoint(UnitPoint) -> ModifiedContent` | -|:white_check_mark:| `func accessibilityAction(AccessibilityActionKind, () -> Void) -> ModifiedContent` | +|:white_check_mark:| `func accessibilityLabel(...) -> some View` | +|:white_check_mark:| `func accessibilityValue(...) -> some View` | +|:white_check_mark:| `func accessibilityHidden(Bool) -> some View` | +|:white_check_mark:| `func accessibilityIdentifier(String) -> some View` | +|:white_check_mark:| `func accessibilityHint(...) -> some View` | +|:white_check_mark:| `func accessibilityActivationPoint(...) -> some View` | +|:white_check_mark:| `func accessibilityAction(...) -> some View` | |:technologist:| `func accessibilityAction