diff --git a/.swiftlint.yml b/.swiftlint.yml index 366b5ac6..8ff3e7bf 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -39,7 +39,7 @@ identifier_name: min_length: 2 file_length: - warning: 500 + warning: 600 error: 1000 function_parameter_count: diff --git a/Package.swift b/Package.swift index 047daed4..d06f697d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/ViewInspector/BaseTypes.swift b/Sources/ViewInspector/BaseTypes.swift index 11b67c95..2e54f237 100644 --- a/Sources/ViewInspector/BaseTypes.swift +++ b/Sources/ViewInspector/BaseTypes.swift @@ -3,54 +3,8 @@ import SwiftUI // MARK: - Protocols @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public protocol Inspectable { - var entity: Content.InspectableEntity { get } - func extractContent(environmentObjects: [AnyObject]) throws -> Any -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Content { - enum InspectableEntity { - case view - case viewModifier - case gesture - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: View { - var entity: Content.InspectableEntity { .view } - - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - var copy = self - environmentObjects.forEach { copy.inject(environmentObject: $0) } - let missingObjects = copy.missingEnvironmentObjects - if missingObjects.count > 0 { - let view = Inspector.typeName(value: self) - throw InspectionError - .missingEnvironmentObjects(view: view, objects: missingObjects) - } - return copy.body - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: ViewModifier { - - var entity: ViewInspector.Content.InspectableEntity { .viewModifier } - - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - var copy = self - environmentObjects.forEach { copy.inject(environmentObject: $0) } - let missingObjects = copy.missingEnvironmentObjects - if missingObjects.count > 0 { - let view = Inspector.typeName(value: self) - throw InspectionError - .missingEnvironmentObjects(view: view, objects: missingObjects) - } - return copy.body() - } -} +@available(*, deprecated, message: "Conformance to Inspectable is no longer required") +public protocol Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public protocol SingleViewContent { @@ -100,7 +54,7 @@ public protocol KnownViewType { public extension KnownViewType { static var namespacedPrefixes: [String] { guard !typePrefix.isEmpty else { return [] } - return ["SwiftUI." + typePrefix] + return [.swiftUINamespaceRegex + typePrefix] } static var isTransitive: Bool { false } static func inspectionCall(typeName: String) -> String { @@ -113,6 +67,35 @@ internal extension String { var firstLetterLowercased: String { prefix(1).lowercased() + dropFirst() } + + static var swiftUINamespaceRegex: String { "SwiftUI(|[a-zA-Z0-9]{0,2}\\))\\." } + + func removingSwiftUINamespace() -> String { + return removing(prefix: .swiftUINamespaceRegex) + .removing(prefix: "SwiftUI.") + } + + func removing(prefix: String) -> String { + guard hasPrefix(prefix) else { return self } + return String(suffix(count - prefix.count)) + } + + fileprivate func hasPrefix(regex: String, wholeMatch: Bool) -> Bool { + let range = NSRange(location: 0, length: utf16.count) + guard let ex = try? NSRegularExpression(pattern: regex), + let match = ex.firstMatch(in: self, range: range) + else { return false } + if wholeMatch { + return match.range == range + } + return match.range.lowerBound == 0 + } +} + +internal extension Array where Element == String { + func containsPrefixRegex(matching name: String, wholeMatch: Bool = true) -> Bool { + return contains(where: { name.hasPrefix(regex: $0, wholeMatch: wholeMatch) }) + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) diff --git a/Sources/ViewInspector/ContentExtraction.swift b/Sources/ViewInspector/ContentExtraction.swift new file mode 100644 index 00000000..4ff2669f --- /dev/null +++ b/Sources/ViewInspector/ContentExtraction.swift @@ -0,0 +1,136 @@ +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +internal struct ContentExtractor { + + internal init(source: Any) throws { + self.contentSource = try Self.contentSource(from: source) + } + + internal func extractContent(environmentObjects: [AnyObject]) throws -> Any { + try validateSource() + + switch contentSource { + case .view(let view): + return try view.extractContent(environmentObjects: environmentObjects) + case .viewModifier(let viewModifier): + return try viewModifier.extractContent(environmentObjects: environmentObjects) + case .gesture(let gesture): + return try gesture.extractContent(environmentObjects: environmentObjects) + } + } + + private static func contentSource(from source: Any) throws -> ContentSource { + switch source { + case let view as any View: + guard !Inspector.isSystemType(value: view) else { + let name = Inspector.typeName(value: source) + throw InspectionError.notSupported("Not a custom view type: \(name)") + } + return .view(view) + case let viewModifier as any ViewModifier: + return .viewModifier(viewModifier) + case let gesture as any Gesture: + return .gesture(gesture) + default: + let name = Inspector.typeName(value: source) + throw InspectionError.notSupported("Not a content type: \(name)") + } + } + + private func validateSource() throws { + switch contentSource.source { + #if os(macOS) + case is any NSViewRepresentable: + throw InspectionError.notSupported( + """ + Please use `.actualView().nsView()` for inspecting \ + the contents of NSViewRepresentable + """) + case is any NSViewControllerRepresentable: + throw InspectionError.notSupported( + """ + Please use `.actualView().viewController()` for inspecting \ + the contents of NSViewControllerRepresentable + """) + #endif + #if os(iOS) || os(tvOS) + case is any UIViewRepresentable: + throw InspectionError.notSupported( + """ + Please use `.actualView().uiView()` for inspecting \ + the contents of UIViewRepresentable + """) + case is any UIViewControllerRepresentable: + throw InspectionError.notSupported( + """ + Please use `.actualView().viewController()` for inspecting \ + the contents of UIViewControllerRepresentable + """) + #endif + #if os(watchOS) + case is any WKInterfaceObjectRepresentable: + throw InspectionError.notSupported( + """ + Please use `.actualView().interfaceObject()` for inspecting \ + the contents of WKInterfaceObjectRepresentable + """) + #endif + default: + return + } + } + + private enum ContentSource { + case view(any View) + case viewModifier(any ViewModifier) + case gesture(any Gesture) + + var source: Any { + switch self { + case .view(let source): return source + case .viewModifier(let source): return source + case .gesture(let source): return source + } + } + } + + private let contentSource: ContentSource +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +public extension View { + + func extractContent(environmentObjects: [AnyObject]) throws -> Any { + var copy = self + environmentObjects.forEach { copy = EnvironmentInjection.inject(environmentObject: $0, into: copy) } + let missingObjects = EnvironmentInjection.missingEnvironmentObjects(for: copy) + if missingObjects.count > 0 { + let view = Inspector.typeName(value: self) + throw InspectionError + .missingEnvironmentObjects(view: view, objects: missingObjects) + } + return copy.body + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +public extension ViewModifier { + + func extractContent(environmentObjects: [AnyObject]) throws -> Any { + var copy = self + environmentObjects.forEach { copy = EnvironmentInjection.inject(environmentObject: $0, into: copy) } + let missingObjects = EnvironmentInjection.missingEnvironmentObjects(for: copy) + if missingObjects.count > 0 { + let view = Inspector.typeName(value: self) + throw InspectionError + .missingEnvironmentObjects(view: view, objects: missingObjects) + } + return copy.body() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +public extension Gesture { + func extractContent(environmentObjects: [AnyObject]) throws -> Any { () } +} diff --git a/Sources/ViewInspector/EnvironmentInjection.swift b/Sources/ViewInspector/EnvironmentInjection.swift index 4c6cb3d6..3b9c239f 100644 --- a/Sources/ViewInspector/EnvironmentInjection.swift +++ b/Sources/ViewInspector/EnvironmentInjection.swift @@ -3,10 +3,11 @@ import SwiftUI // MARK: - EnvironmentObject injection @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -internal extension Inspectable { - var missingEnvironmentObjects: [String] { + +internal enum EnvironmentInjection { + static func missingEnvironmentObjects(for entity: Any) -> [String] { let prefix = "SwiftUI.EnvironmentObject<" - let mirror = Mirror(reflecting: self) + let mirror = Mirror(reflecting: entity) return mirror.children.compactMap { let fullName = Inspector.typeName(value: $0.value, namespaced: true) guard fullName.hasPrefix(prefix), @@ -18,24 +19,24 @@ internal extension Inspectable { return "\(ivarName[1..(environmentObject: AnyObject, into entity: T) -> T { let type = "SwiftUI.EnvironmentObject<\(Inspector.typeName(value: environmentObject, namespaced: true))>" - let mirror = Mirror(reflecting: self) + let mirror = Mirror(reflecting: entity) guard let label = mirror.children .first(where: { Inspector.typeName(value: $0.value, namespaced: true) == type })?.label - else { return } + else { return entity } let envObjSize = EnvObject.structSize - let viewSize = MemoryLayout.size - var offset = MemoryLayout.stride - envObjSize - let step = MemoryLayout.alignment + let viewSize = MemoryLayout.size + var offset = MemoryLayout.stride - envObjSize + let step = MemoryLayout.alignment while offset + envObjSize > viewSize { offset -= step } - withUnsafeBytes(of: EnvObject.Forgery(object: nil)) { reference in + return withUnsafeBytes(of: EnvObject.Forgery(object: nil)) { reference in while offset >= 0 { - var copy = self + var copy = entity withUnsafeMutableBytes(of: ©) { bytes in guard bytes[offset.. Iterator.Element { do { do { - let viewes = try View.children(content) - return try .init(try viewes.element(at: index), parent: self, call: "[\(index)]") + return try .init(try child(at: index), parent: self, call: "[\(index)]") } catch InspectionError.viewNotFound { return try Element(.absentView, parent: self, index: index) } catch { throw error } diff --git a/Sources/ViewInspector/InspectableView.swift b/Sources/ViewInspector/InspectableView.swift index 1b52ce95..0456ad85 100644 --- a/Sources/ViewInspector/InspectableView.swift +++ b/Sources/ViewInspector/InspectableView.swift @@ -16,42 +16,73 @@ public struct InspectableView where View: KnownViewType { ? parent?.parentView : parent let inspectionCall = index .flatMap({ call.replacingOccurrences(of: "_:", with: "\($0)") }) ?? call - try self.init(content: content, parent: parentView, call: inspectionCall, index: index) + self = try Self.build(content: content, parent: parentView, call: inspectionCall, index: index) } - private init(content: Content, parent: UnwrappedView?, call: String, index: Int?) throws { + private static func build(content: Content, parent: UnwrappedView?, call: String, index: Int?) throws -> Self { if !View.typePrefix.isEmpty, Inspector.isTupleView(content.view), View.self != ViewType.TupleView.self { throw InspectionError.notSupported( "Unable to extract \(View.typePrefix): please specify its index inside parent view") } - self.content = content - self.parentView = parent - self.inspectionCall = call - self.inspectionIndex = index do { try Inspector.guardType(value: content.view, namespacedPrefixes: View.namespacedPrefixes, - inspectionCall: inspectionCall) + inspectionCall: call) } catch { if let err = error as? InspectionError, case .typeMismatch = err { + if let child = try? implicitCustomViewChild( + parent: parent, call: call, index: index) { + return child + } let factual = Inspector.typeName(value: content.view, namespaced: true) .removingSwiftUINamespace() let expected = View.namespacedPrefixes .map { $0.removingSwiftUINamespace() } .joined(separator: " or ") + let pathToRoot = Self.init(content: content, parent: parent, call: call, index: index) + .pathToRoot throw InspectionError.inspection(path: pathToRoot, factual: factual, expected: expected) } throw error } + return Self.init(content: content, parent: parent, call: call, index: index) + } + + private init(content: Content, parent: UnwrappedView?, call: String, index: Int?) { + self.content = content + self.parentView = parent + self.inspectionCall = call + self.inspectionIndex = index + } + + private static func implicitCustomViewChild(parent: UnwrappedView?, call: String, index: Int?) throws -> Self? { + guard let (content, parent) = try parent?.implicitCustomViewChild(index: index ?? 0, call: call) + else { return nil } + return try build(content: content, parent: parent, call: call, index: index) + } + + fileprivate func withCustomViewInspectionCall() -> Self { + let customViewType = Inspector.typeName(value: content.view, namespaced: false) + let call = "view(\(customViewType).self)" + return .init(content: content, parent: parentView, call: call, index: inspectionIndex) } } -private extension String { - func removingSwiftUINamespace() -> String { - guard hasPrefix("SwiftUI.") else { return self } - return String(suffix(count - 8)) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +extension UnwrappedView { + func implicitCustomViewChild(index: Int, call: String) throws + -> (content: Content, parent: InspectableView>)? { + guard let parent = self as? InspectableView, + !Inspector.isSystemType(value: parent.content.view), + !(parent.content.view is AbsentView), + let customViewParent = try? parent + .asInspectableView(ofType: ViewType.View.self) + else { return nil } + let child = try customViewParent.child(at: index) + let updatedParent = customViewParent.withCustomViewInspectionCall() + return (child, updatedParent) } } @@ -140,6 +171,10 @@ internal extension InspectableView where View: MultipleViewContent { func child(at index: Int, isTupleExtraction: Bool = false) throws -> Content { let viewes = try View.children(content) guard index >= 0 && index < viewes.count else { + if let unwrapped = try Self.implicitCustomViewChild( + parent: self, call: inspectionCall, index: index) { + return unwrapped.content + } throw InspectionError.viewIndexOutOfBounds(index: index, count: viewes.count) } let child = try viewes.element(at: index) @@ -157,36 +192,19 @@ internal extension InspectableView where View: MultipleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension View { + func inspect(function: String = #function) throws -> InspectableView { let medium = ViewHosting.medium(function: function) let content = try Inspector.unwrap(view: self, medium: medium) return try .init(content, parent: nil, call: "") } - func inspect(function: String = #function, file: StaticString = #file, line: UInt = #line, - inspection: (InspectableView) throws -> Void) { - do { - try inspection(try inspect(function: function)) - } catch { - XCTFail("\(error.localizedDescription)", file: file, line: line) - } - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension View where Self: Inspectable { - - func inspect(function: String = #function) throws -> InspectableView> { - let call = "view(\(ViewType.View.typePrefix).self)" - let medium = ViewHosting.medium(function: function) - let content = Content(self, medium: medium) - return try .init(content, parent: nil, call: call) - } - func inspect(function: String = #function, file: StaticString = #file, line: UInt = #line, inspection: (InspectableView>) throws -> Void) { do { - try inspection(try inspect(function: function)) + let view = try inspect(function: function) + .asInspectableView(ofType: ViewType.View.self) + try inspection(view) } catch { XCTFail("\(error.localizedDescription)", file: file, line: line) } @@ -196,7 +214,8 @@ public extension View where Self: Inspectable { // MARK: - Modifiers @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension ViewModifier where Self: Inspectable { +public extension ViewModifier { + func inspect(function: String = #function) throws -> InspectableView> { let medium = ViewHosting.medium(function: function) let content = try Inspector.unwrap(view: self, medium: medium) @@ -310,7 +329,7 @@ internal extension Content { internal protocol ModifierNameProvider { var modifierType: String { get } func modifierType(prefixOnly: Bool) -> String - var customModifier: Inspectable? { get } + var customModifier: Any? { get } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) @@ -325,8 +344,11 @@ extension ModifiedContent: ModifierNameProvider { return Inspector.typeName(type: Modifier.self, generics: prefixOnly ? .remove : .keep) } - var customModifier: Inspectable? { - return try? Inspector.attribute(label: "modifier", value: self, type: Inspectable.self) + var customModifier: Any? { + guard let modifier = try? Inspector.attribute(label: "modifier", value: self), + !Inspector.isSystemType(value: modifier) + else { return nil } + return modifier } } diff --git a/Sources/ViewInspector/InspectionEmissary.swift b/Sources/ViewInspector/InspectionEmissary.swift index f58c8dd8..afd972c7 100644 --- a/Sources/ViewInspector/InspectionEmissary.swift +++ b/Sources/ViewInspector/InspectionEmissary.swift @@ -13,7 +13,7 @@ public protocol InspectionEmissary: AnyObject { // MARK: - InspectionEmissary for View @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension InspectionEmissary where V: View & Inspectable { +public extension InspectionEmissary where V: View { typealias ViewInspection = (InspectableView>) throws -> Void @@ -23,7 +23,9 @@ public extension InspectionEmissary where V: View & Inspectable { _ inspection: @escaping ViewInspection ) -> XCTestExpectation { return inspect(after: delay, function: function, file: file, line: line) { view in - return try inspection(try view.inspect(function: function)) + let unwrapped = try view.inspect(function: function) + .asInspectableView(ofType: ViewType.View.self) + return try inspection(unwrapped) } } @@ -33,7 +35,9 @@ public extension InspectionEmissary where V: View & Inspectable { _ inspection: @escaping ViewInspection ) -> XCTestExpectation where P: Publisher, P.Failure == Never { return inspect(onReceive: publisher, function: function, file: file, line: line) { view in - return try inspection(try view.inspect(function: function)) + let unwrapped = try view.inspect(function: function) + .asInspectableView(ofType: ViewType.View.self) + return try inspection(unwrapped) } } } @@ -41,7 +45,7 @@ public extension InspectionEmissary where V: View & Inspectable { // MARK: - InspectionEmissary for ViewModifier @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension InspectionEmissary where V: ViewModifier & Inspectable { +public extension InspectionEmissary where V: ViewModifier { typealias ViewModifierInspection = (InspectableView>) throws -> Void @@ -122,41 +126,44 @@ private extension InspectionEmissary { // MARK: - on keyPath inspection @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension View where Self: Inspectable { +public extension View { @discardableResult mutating func on(_ keyPath: WritableKeyPath Void)?>, function: String = #function, file: StaticString = #file, line: UInt = #line, perform: @escaping ((InspectableView>) throws -> Void) ) -> XCTestExpectation { - return on(keyPath, function: function, file: file, line: line) { body in + return Inspector.injectInspectionCallback( + value: &self, keyPath: keyPath, function: function, file: file, line: line) { body in body.inspect(function: function, file: file, line: line, inspection: perform) } } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension ViewModifier where Self: Inspectable { +public extension ViewModifier { @discardableResult mutating func on(_ keyPath: WritableKeyPath Void)?>, function: String = #function, file: StaticString = #file, line: UInt = #line, perform: @escaping ((InspectableView>) throws -> Void) ) -> XCTestExpectation { - return on(keyPath, function: function, file: file, line: line) { body in + return Inspector.injectInspectionCallback( + value: &self, keyPath: keyPath, function: function, file: file, line: line) { body in body.inspect(function: function, file: file, line: line, inspection: perform) } } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private extension Inspectable { - mutating func on(_ keyPath: WritableKeyPath Void)?>, - function: String, file: StaticString, line: UInt, - inspect: @escaping ((Self) -> Void) +private extension Inspector { + static func injectInspectionCallback( + value: inout T, keyPath: WritableKeyPath Void)?>, + function: String, file: StaticString, line: UInt, + inspection: @escaping ((T) -> Void) ) -> XCTestExpectation { let description = Inspector.typeName(value: self) + " callback at line #\(line)" let expectation = XCTestExpectation(description: description) - self[keyPath: keyPath] = { body in - inspect(body) + value[keyPath: keyPath] = { body in + inspection(body) ViewHosting.expel(function: function) expectation.fulfill() } diff --git a/Sources/ViewInspector/Inspector.swift b/Sources/ViewInspector/Inspector.swift index 2cfcf079..ead3d09a 100644 --- a/Sources/ViewInspector/Inspector.swift +++ b/Sources/ViewInspector/Inspector.swift @@ -54,6 +54,24 @@ internal extension Inspector { generics: generics) } + static func isSystemType(value: Any) -> Bool { + let name = typeName(value: value, namespaced: true) + return isSystemType(name: name) + } + + static func isSystemType(type: Any.Type) -> Bool { + let name = typeName(type: type, namespaced: true) + return isSystemType(name: name) + } + + private static func isSystemType(name: String) -> Bool { + return [ + String.swiftUINamespaceRegex, + "_CoreLocationUI_SwiftUI\\.", "_MapKit_SwiftUI\\.", + "_AuthenticationServices_SwiftUI\\.", "_AVKit_SwiftUI\\.", + ].containsPrefixRegex(matching: name, wholeMatch: false) + } + static func typeName(type: Any.Type, namespaced: Bool = false, generics: GenericParameters = .keep) -> String { @@ -164,8 +182,8 @@ public extension Inspector { dict[childName + ": " + childType] = attributesTree( value: child.element.value, medium: medium, visited: visited) } - if let inspectable = value as? Inspectable, - let content = try? inspectable.extractContent(environmentObjects: medium.environmentObjects) { + if let contentExtractor = try? ContentExtractor(source: value), + let content = try? contentExtractor.extractContent(environmentObjects: medium.environmentObjects) { let childType = typeName(value: content) dict["body: " + childType] = attributesTree(value: content, medium: medium, visited: visited) } @@ -266,10 +284,11 @@ internal extension Inspector { """) } } - if namespacedPrefixes.contains(typePrefix) { + if namespacedPrefixes.containsPrefixRegex(matching: typePrefix) { return } - if let prefix = namespacedPrefixes.first { + if var prefix = namespacedPrefixes.first { + prefix = prefix.replacingOccurrences(of: String.swiftUINamespaceRegex, with: "SwiftUI.") let typePrefix = typeName(type: type(of: value), namespaced: true) throw InspectionError.typeMismatch(factual: typePrefix, expected: prefix) } diff --git a/Sources/ViewInspector/Modifiers/ConfigurationModifiers.swift b/Sources/ViewInspector/Modifiers/ConfigurationModifiers.swift index 707c6b49..73c27d47 100644 --- a/Sources/ViewInspector/Modifiers/ConfigurationModifiers.swift +++ b/Sources/ViewInspector/Modifiers/ConfigurationModifiers.swift @@ -19,10 +19,12 @@ public extension InspectableView { #if os(macOS) func horizontalRadioGroupLayout() throws -> Bool { - _ = try modifierAttribute( - modifierName: "RadioGroupLayoutModifier<_HStackLayout>", - path: "modifier|style", - type: Any.self, call: "horizontalRadioGroupLayout") + _ = try modifier({ modifier -> Bool in + return [ + "RadioGroupStyleModifier>", + "RadioGroupLayoutModifier<_HStackLayout>", + ].contains(where: { modifier.modifierType == $0 }) + }, call: "horizontalRadioGroupLayout") return true } diff --git a/Sources/ViewInspector/Modifiers/NavigationBarModifiers.swift b/Sources/ViewInspector/Modifiers/NavigationBarModifiers.swift index 375b239b..3e262587 100644 --- a/Sources/ViewInspector/Modifiers/NavigationBarModifiers.swift +++ b/Sources/ViewInspector/Modifiers/NavigationBarModifiers.swift @@ -1,6 +1,6 @@ import SwiftUI -#if os(macOS) && !MAC_OS_VERSION_13_0 +#if (os(macOS) || targetEnvironment(macCatalyst)) && !MAC_OS_VERSION_13_0 struct ToolbarPlacement { static var navigationBar: ToolbarPlacement { .init() } } diff --git a/Sources/ViewInspector/SwiftUI/ActionSheet.swift b/Sources/ViewInspector/SwiftUI/ActionSheet.swift index 9ff0bafc..fb7d6a98 100644 --- a/Sources/ViewInspector/SwiftUI/ActionSheet.swift +++ b/Sources/ViewInspector/SwiftUI/ActionSheet.swift @@ -1,7 +1,5 @@ import SwiftUI -// MARK: - ActionSheet - @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension ViewType { diff --git a/Sources/ViewInspector/SwiftUI/Alert.swift b/Sources/ViewInspector/SwiftUI/Alert.swift index 61141225..aca39044 100644 --- a/Sources/ViewInspector/SwiftUI/Alert.swift +++ b/Sources/ViewInspector/SwiftUI/Alert.swift @@ -9,7 +9,7 @@ public extension ViewType { public static var typePrefix: String = ViewType.PopupContainer.typePrefix static var typePrefixIOS15: String = "AlertModifier" public static var namespacedPrefixes: [String] { - [typePrefix, "SwiftUI." + typePrefixIOS15] + [typePrefix, .swiftUINamespaceRegex + typePrefixIOS15] } public static func inspectionCall(typeName: String) -> String { return "alert(\(ViewType.indexPlaceholder))" diff --git a/Sources/ViewInspector/SwiftUI/Button.swift b/Sources/ViewInspector/SwiftUI/Button.swift index 7dfad8df..49b757b3 100644 --- a/Sources/ViewInspector/SwiftUI/Button.swift +++ b/Sources/ViewInspector/SwiftUI/Button.swift @@ -73,7 +73,10 @@ public extension InspectableView { func buttonStyle() throws -> Any { let modifier = try self.modifier({ modifier -> Bool in - return modifier.modifierType.hasPrefix("ButtonStyleModifier") + return [ + "ButtonStyleContainerModifier", + "ButtonStyleModifier", + ].contains(where: { modifier.modifierType.hasPrefix($0) }) }, call: "buttonStyle") if let style = try? Inspector.attribute(path: "modifier|style|style", value: modifier) { return style diff --git a/Sources/ViewInspector/SwiftUI/CustomView.swift b/Sources/ViewInspector/SwiftUI/CustomView.swift index 26f115fe..9f9ad4dd 100644 --- a/Sources/ViewInspector/SwiftUI/CustomView.swift +++ b/Sources/ViewInspector/SwiftUI/CustomView.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public protocol CustomViewType { - associatedtype T: Inspectable + associatedtype T } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) @@ -10,7 +10,7 @@ public extension ViewType { internal static let customViewGenericsPlaceholder = "" - struct View: KnownViewType, CustomViewType where T: Inspectable { + struct View: KnownViewType, CustomViewType { public static var typePrefix: String { guard T.self != ViewType.Stub.self else { return "" } @@ -50,19 +50,19 @@ extension ViewType.View: MultipleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension Content { var isCustomView: Bool { - return view is Inspectable + return !Inspector.isSystemType(value: view) } func extractCustomView() throws -> Content { - let inspectable = try Inspector.cast(value: view, type: Inspectable.self) - let view = try inspectable.extractContent(environmentObjects: medium.environmentObjects) + let contentExtractor = try ContentExtractor(source: view) + let view = try contentExtractor.extractContent(environmentObjects: medium.environmentObjects) let medium = self.medium.resettingViewModifiers() return try Inspector.unwrap(view: view, medium: medium) } func extractCustomViewGroup() throws -> LazyGroup { - let inspectable = try Inspector.cast(value: view, type: Inspectable.self) - let view = try inspectable.extractContent(environmentObjects: medium.environmentObjects) + let contentExtractor = try ContentExtractor(source: view) + let view = try contentExtractor.extractContent(environmentObjects: medium.environmentObjects) return try Inspector.viewsInContainer(view: view, medium: medium) } } @@ -72,7 +72,15 @@ internal extension Content { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension InspectableView where View: SingleViewContent { - func view(_ type: T.Type) throws -> InspectableView> where T: Inspectable { + func view(_ type: T.Type) throws -> InspectableView> where T: SwiftUI.View { + guard !Inspector.isSystemType(type: type) else { + let name = Inspector.typeName(type: type) + throw InspectionError.notSupported( + """ + Please replace .view(\(name).self) inspection call with \ + .\(name.firstLetterLowercased)() or .find(ViewType.\(name).self) + """) + } let child = try View.child(content) let prefix = Inspector.typeName(type: type, namespaced: true, generics: .remove) let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) @@ -87,7 +95,15 @@ public extension InspectableView where View: SingleViewContent { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension InspectableView where View: MultipleViewContent { - func view(_ type: T.Type, _ index: Int) throws -> InspectableView> where T: Inspectable { + func view(_ type: T.Type, _ index: Int) throws -> InspectableView> where T: SwiftUI.View { + guard !Inspector.isSystemType(type: type) else { + let name = Inspector.typeName(type: type) + throw InspectionError.notSupported( + """ + Please replace .view(\(name).self, \(index)) inspection \ + call with .\(name.firstLetterLowercased)(\(index)) + """) + } let content = try child(at: index) let prefix = Inspector.typeName(type: type, namespaced: true, generics: .remove) let base = ViewType.View.inspectionCall(typeName: Inspector.typeName(type: type)) @@ -105,7 +121,7 @@ public extension InspectableView where View: CustomViewType { func actualView() throws -> View.T { var view = try Inspector.cast(value: content.view, type: View.T.self) content.medium.environmentObjects.forEach { - view.inject(environmentObject: $0) + view = EnvironmentInjection.inject(environmentObject: $0, into: view) } return view } @@ -113,81 +129,39 @@ public extension InspectableView where View: CustomViewType { #if os(macOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension NSViewRepresentable where Self: Inspectable { +public extension NSViewRepresentable { func nsView() throws -> NSViewType { return try ViewHosting.lookup(Self.self) } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension NSViewControllerRepresentable where Self: Inspectable { +public extension NSViewControllerRepresentable { func viewController() throws -> NSViewControllerType { return try ViewHosting.lookup(Self.self) } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: NSViewRepresentable { - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - throw InspectionError.notSupported( - "Please use `.actualView().nsView()` for inspecting the contents of NSViewRepresentable") - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: NSViewControllerRepresentable { - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - throw InspectionError.notSupported( - "Please use `.actualView().viewController()` for inspecting the contents of NSViewControllerRepresentable") - } -} #elseif os(iOS) || os(tvOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension UIViewRepresentable where Self: Inspectable { +public extension UIViewRepresentable { func uiView() throws -> UIViewType { return try ViewHosting.lookup(Self.self) } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension UIViewControllerRepresentable where Self: Inspectable { +public extension UIViewControllerRepresentable { func viewController() throws -> UIViewControllerType { return try ViewHosting.lookup(Self.self) } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: UIViewRepresentable { - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - throw InspectionError.notSupported( - "Please use `.actualView().uiView()` for inspecting the contents of UIViewRepresentable") - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Inspectable where Self: UIViewControllerRepresentable { - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - throw InspectionError.notSupported( - "Please use `.actualView().viewController()` for inspecting the contents of UIViewControllerRepresentable") - } -} #elseif os(watchOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) -public extension WKInterfaceObjectRepresentable where Self: Inspectable { +public extension WKInterfaceObjectRepresentable { func interfaceObject() throws -> WKInterfaceObjectType { return try ViewHosting.lookup(Self.self) } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) -public extension Inspectable where Self: WKInterfaceObjectRepresentable { - func extractContent(environmentObjects: [AnyObject]) throws -> Any { - throw InspectionError.notSupported( - """ - Please use `.actualView().interfaceObject()` for inspecting \ - the contents of WKInterfaceObjectRepresentable - """) - } -} #endif diff --git a/Sources/ViewInspector/SwiftUI/CustomViewModifier.swift b/Sources/ViewInspector/SwiftUI/CustomViewModifier.swift index 5b0b6c3d..0251ae31 100644 --- a/Sources/ViewInspector/SwiftUI/CustomViewModifier.swift +++ b/Sources/ViewInspector/SwiftUI/CustomViewModifier.swift @@ -2,8 +2,8 @@ import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension ViewType { - - struct ViewModifier: KnownViewType, CustomViewType where T: Inspectable { + + struct ViewModifier: KnownViewType, CustomViewType { public static var typePrefix: String { "" } @@ -19,9 +19,9 @@ public extension ViewType { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension InspectableView { - + func modifier(_ type: T.Type, _ index: Int? = nil) throws -> InspectableView> - where T: ViewModifier, T: Inspectable { + where T: ViewModifier { let name = Inspector.typeName(type: type) guard let view = content.medium.viewModifiers.reversed().compactMap({ modifier in try? Inspector.attribute(label: "modifier", value: modifier, type: type) @@ -108,10 +108,10 @@ internal extension Content { return try Inspector.unwrap(view: view, medium: medium) } - func customViewModifiers() -> [Inspectable] { - return medium.viewModifiers.reversed().compactMap({ modifier in - try? Inspector.attribute(label: "modifier", value: modifier, type: Inspectable.self) - }) + func customViewModifiers() -> [Any] { + return medium.viewModifiers.reversed() + .compactMap { try? Inspector.attribute(label: "modifier", value: $0) } + .filter { !Inspector.isSystemType(value: $0) } } } diff --git a/Sources/ViewInspector/SwiftUI/Gesture.swift b/Sources/ViewInspector/SwiftUI/Gesture.swift index a743e859..e4105c29 100644 --- a/Sources/ViewInspector/SwiftUI/Gesture.swift +++ b/Sources/ViewInspector/SwiftUI/Gesture.swift @@ -2,29 +2,29 @@ import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public protocol GestureViewType { - associatedtype T: SwiftUI.Gesture & Inspectable + associatedtype T: SwiftUI.Gesture } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension ViewType { struct Gesture: KnownViewType, GestureViewType - where T: SwiftUI.Gesture & Inspectable { + where T: SwiftUI.Gesture { public static var typePrefix: String { return Inspector.typeName(type: T.self, generics: .remove) } public static var namespacedPrefixes: [String] { var prefixes = [ - "SwiftUI.AddGestureModifier", - "SwiftUI.HighPriorityGestureModifier", - "SwiftUI.SimultaneousGestureModifier", - "SwiftUI._ChangedGesture", - "SwiftUI._EndedGesture", - "SwiftUI._MapGesture", - "SwiftUI._ModifiersGesture", - "SwiftUI.GestureStateGesture" - ] + "AddGestureModifier", + "HighPriorityGestureModifier", + "SimultaneousGestureModifier", + "_ChangedGesture", + "_EndedGesture", + "_MapGesture", + "_ModifiersGesture", + "GestureStateGesture" + ].map { String.swiftUINamespaceRegex + $0 } prefixes.append(Inspector.typeName(type: T.self, namespaced: true, generics: .remove)) return prefixes } @@ -39,63 +39,25 @@ public extension ViewType { } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -public extension Gesture where Self: Inspectable { - var entity: Content.InspectableEntity { .gesture } - func extractContent(environmentObjects: [AnyObject]) throws -> Any { () } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension AnyGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, *) -@available(tvOS, unavailable) -extension DragGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension ExclusiveGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, tvOS 14.0, *) -extension LongPressGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, *) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -extension MagnificationGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, *) -@available(tvOS, unavailable) -@available(watchOS, unavailable) -extension RotationGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension SequenceGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension SimultaneousGesture: Inspectable {} - -@available(iOS 13.0, macOS 10.15, tvOS 16.0, *) -extension TapGesture: Inspectable {} - @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public extension InspectableView { func gesture(_ type: T.Type, _ index: Int? = nil) throws -> InspectableView> - where T: Gesture & Inspectable { + where T: Gesture { return try gestureModifier( modifierName: "AddGestureModifier", path: "modifier", type: type, call: "gesture", index: index) } func highPriorityGesture(_ type: T.Type, _ index: Int? = nil) throws -> InspectableView> - where T: Gesture & Inspectable { + where T: Gesture { return try gestureModifier( modifierName: "HighPriorityGestureModifier", path: "modifier", type: type, call: "highPriorityGesture", index: index) } func simultaneousGesture(_ type: T.Type, _ index: Int? = nil) throws -> InspectableView> - where T: Gesture & Inspectable { + where T: Gesture { return try gestureModifier( modifierName: "SimultaneousGestureModifier", path: "modifier", type: type, call: "simultaneousGesture", index: index) @@ -170,7 +132,7 @@ public extension InspectableView { @available(iOS, unavailable) @available(tvOS, unavailable) func gestureModifiers() throws -> EventModifiers - where T: Gesture & Inspectable, View == ViewType.Gesture { + where T: Gesture, View == ViewType.Gesture { let typeName = Inspector.typeName(type: T.self) let valueName = Inspector.typeName(value: content.view) let (_, modifiers) = gestureInfo(typeName, valueName) @@ -200,7 +162,7 @@ internal extension InspectableView { type: T.Type, call: String, index: Int? = nil) throws -> InspectableView> - where T: Gesture, T: Inspectable { + where T: Gesture { let typeName = Inspector.typeName(type: type) let modifierCall = ViewType.Gesture.inspectionCall(call: call, typeName: typeName, index: nil) diff --git a/Sources/ViewInspector/SwiftUI/IDView.swift b/Sources/ViewInspector/SwiftUI/IDView.swift index 580a98cd..856b2b23 100644 --- a/Sources/ViewInspector/SwiftUI/IDView.swift +++ b/Sources/ViewInspector/SwiftUI/IDView.swift @@ -23,7 +23,7 @@ extension ViewType.IDView: SingleViewContent { private struct IDViewModifier: ModifierNameProvider { static var modifierName: String { "IDView" } func modifierType(prefixOnly: Bool) -> String { IDViewModifier.modifierName } - var customModifier: Inspectable? { nil } + var customModifier: Any? { nil } let view: Any } diff --git a/Sources/ViewInspector/SwiftUI/PasteButton.swift b/Sources/ViewInspector/SwiftUI/PasteButton.swift index b7f9cecf..dc0640d5 100644 --- a/Sources/ViewInspector/SwiftUI/PasteButton.swift +++ b/Sources/ViewInspector/SwiftUI/PasteButton.swift @@ -48,8 +48,9 @@ public extension InspectableView where View == ViewType.PasteButton { @available(macOS 11.0, *) func supportedContentTypes() throws -> [UTType] { + let container = (try? Inspector.attribute(label: "pasteHelper", value: content.view)) ?? content.view return try Inspector - .attribute(label: "supportedContentTypes", value: content.view, type: [UTType].self) + .attribute(label: "supportedContentTypes", value: container, type: [UTType].self) } } #endif diff --git a/Sources/ViewInspector/SwiftUI/Shape.swift b/Sources/ViewInspector/SwiftUI/Shape.swift index 0072a1a9..50b4a56e 100644 --- a/Sources/ViewInspector/SwiftUI/Shape.swift +++ b/Sources/ViewInspector/SwiftUI/Shape.swift @@ -8,6 +8,10 @@ public extension ViewType { public static func inspectionCall(typeName: String) -> String { return "shape(\(ViewType.indexPlaceholder))" } + + fileprivate static func inspectionCall(index: Int) -> String { + return ViewType.inspectionCall(base: inspectionCall(typeName: ""), index: index) + } } } @@ -18,6 +22,12 @@ public extension InspectableView where View: SingleViewContent { func shape() throws -> InspectableView { let content = try child() + let call = ViewType.Shape.inspectionCall(index: 0) + if !content.isShape, + let child = try? implicitCustomViewChild(index: 0, call: call)?.content, + child.isShape { + return try .init(child, parent: self) + } try content.throwIfNotShape() return try .init(content, parent: self) } @@ -30,6 +40,12 @@ public extension InspectableView where View: MultipleViewContent { func shape(_ index: Int) throws -> InspectableView { let content = try child(at: index) + let call = ViewType.Shape.inspectionCall(index: index) + if !content.isShape, + let child = try? implicitCustomViewChild(index: index, call: call)?.content, + child.isShape { + return try .init(child, parent: self) + } try content.throwIfNotShape() return try .init(content, parent: self, index: index) } diff --git a/Sources/ViewInspector/SwiftUI/Text.swift b/Sources/ViewInspector/SwiftUI/Text.swift index 2a511a0d..8cf44e2a 100644 --- a/Sources/ViewInspector/SwiftUI/Text.swift +++ b/Sources/ViewInspector/SwiftUI/Text.swift @@ -65,6 +65,16 @@ public extension InspectableView where View == ViewType.Text { func images() throws -> [Image] { return try ViewType.Text.extractImages(from: self) } + + /** + Returns `AttributedString` only for `Text` views constructed this way. + If you used explicit style modifiers, for example, `Text("Hi").bold()`, + consider using `attributes()` instead. + */ + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + func attributedString() throws -> AttributedString { + return try ViewType.Text.extractAttributedString(from: self) + } } // MARK: - String extraction @@ -92,6 +102,11 @@ private extension ViewType.Text { return try extractString(dateTextStorage: textStorage) case "FormatterTextStorage": return try extractString(formatterTextStorage: textStorage) + case "AttributedStringTextStorage": + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) else { + throw InspectionError.notSupported("AttributedString is not supported in this OS version") + } + return String(try view.attributedString().characters) default: throw InspectionError.notSupported("Unknown text storage: \(storageType)") } @@ -248,3 +263,19 @@ private extension ViewType.Text { return [] } } + +// MARK: - AttributedString extraction + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension ViewType.Text { + static func extractAttributedString(from view: InspectableView) throws -> AttributedString { + let textStorage = try Inspector.attribute(path: "storage|anyTextStorage", value: view.content.view) + let storageType = Inspector.typeName(value: textStorage) + switch storageType { + case "AttributedStringTextStorage": + return try Inspector.attribute(label: "str", value: textStorage, type: AttributedString.self) + default: + throw InspectionError.notSupported("Please use attributes() for accessing the text styles on this view") + } + } +} diff --git a/Sources/ViewInspector/SwiftUI/TimelineView.swift b/Sources/ViewInspector/SwiftUI/TimelineView.swift index fa591941..1cd2488b 100644 --- a/Sources/ViewInspector/SwiftUI/TimelineView.swift +++ b/Sources/ViewInspector/SwiftUI/TimelineView.swift @@ -59,7 +59,8 @@ public extension InspectableView where View == ViewType.TimelineView { func contentView(_ context: ViewType.TimelineView.Context = .init() ) throws -> InspectableView { - return try ViewType.TimelineView.view(for: context, parent: self) + let contextCopy = context + return try ViewType.TimelineView.view(for: contextCopy, parent: self) } } diff --git a/Sources/ViewInspector/SwiftUI/Toolbar.swift b/Sources/ViewInspector/SwiftUI/Toolbar.swift index 0cc065d2..3ed43c34 100644 --- a/Sources/ViewInspector/SwiftUI/Toolbar.swift +++ b/Sources/ViewInspector/SwiftUI/Toolbar.swift @@ -11,13 +11,6 @@ public extension ViewType { return "_ToolbarItemGroupModifier" } }() - public static var namespacedPrefixes: [String] { - #if os(tvOS) - return ["SwiftUI." + typePrefix, "SwiftUI1c)." + typePrefix] - #else - return ["SwiftUI." + typePrefix] - #endif - } public static func inspectionCall(typeName: String) -> String { return "toolbar(\(ViewType.indexPlaceholder))" } diff --git a/Sources/ViewInspector/ViewHosting.swift b/Sources/ViewInspector/ViewHosting.swift index 7e49ec98..cad7f372 100644 --- a/Sources/ViewInspector/ViewHosting.swift +++ b/Sources/ViewInspector/ViewHosting.swift @@ -245,7 +245,7 @@ private class RootViewController: NSViewController { internal extension ViewHosting { #if os(macOS) static func lookup(_ view: V.Type) throws -> V.NSViewType - where V: Inspectable & NSViewRepresentable { + where V: NSViewRepresentable { let name = Inspector.typeName(type: view) let viewHost = rootViewController.view.descendant(nameTraits: ["ViewHost", name]) guard let view = viewHost?.subviews.compactMap({ $0 as? V.NSViewType }).first else { @@ -255,7 +255,7 @@ internal extension ViewHosting { } static func lookup(_ viewController: V.Type) throws -> V.NSViewControllerType - where V: Inspectable & NSViewControllerRepresentable { + where V: NSViewControllerRepresentable { let name = Inspector.typeName(type: viewController) let hostVC = rootViewController.descendant(nameTraits: ["NSHostingController", name]) if let vc = hostVC?.descendants.compactMap({ $0 as? V.NSViewControllerType }).first { @@ -269,7 +269,7 @@ internal extension ViewHosting { } #elseif os(iOS) || os(tvOS) static func lookup(_ view: V.Type) throws -> V.UIViewType - where V: Inspectable & UIViewRepresentable { + where V: UIViewRepresentable { let name = Inspector.typeName(type: view) let viewHost = window.descendant(nameTraits: ["ViewHost", name]) guard let view = viewHost?.subviews.compactMap({ $0 as? V.UIViewType }).first else { @@ -279,7 +279,7 @@ internal extension ViewHosting { } static func lookup(_ viewController: V.Type) throws -> V.UIViewControllerType - where V: Inspectable & UIViewControllerRepresentable { + where V: UIViewControllerRepresentable { let name = Inspector.typeName(type: viewController) let hostVC = window.rootViewController?.descendant(nameTraits: ["UIHostingController", name]) guard let vc = hostVC?.descendants.compactMap({ $0 as? V.UIViewControllerType }) @@ -288,7 +288,7 @@ internal extension ViewHosting { } #elseif os(watchOS) static func lookup(_ view: V.Type) throws -> V.WKInterfaceObjectType - where V: Inspectable & WKInterfaceObjectRepresentable { + where V: WKInterfaceObjectRepresentable { let name = Inspector.typeName(type: view) guard let rootVC = WKExtension.shared().rootInterfaceController, let viewCache = try? Inspector.attribute(path: """ diff --git a/Sources/ViewInspector/ViewSearch.swift b/Sources/ViewInspector/ViewSearch.swift index 6202f4d1..c176a285 100644 --- a/Sources/ViewInspector/ViewSearch.swift +++ b/Sources/ViewInspector/ViewSearch.swift @@ -166,34 +166,44 @@ public extension InspectableView { /** Searches for a view of a specific type that matches a given condition - - Parameter inspectable: Your custom `Inspectable` view type. For example: `ContentView.self` + - Parameter customViewType: Your custom view type. For example: `ContentView.self` - Parameter relation: The direction of the search. Defaults to `.child` - Parameter where: The condition closure for detecting a matching view. Thrown errors are interpreted as "this view does not match" - Throws: An error if the view cannot be found - Returns: A found view */ - func find(_ inspectable: V.Type, + func find(_ customViewType: V.Type, relation: ViewSearch.Relation = .child, where condition: (InspectableView>) throws -> Bool = { _ in true } - ) throws -> InspectableView> where V: Inspectable { + ) throws -> InspectableView> where V: SwiftUI.View { + guard !Inspector.isSystemType(type: customViewType) else { + let name = Inspector.typeName(type: customViewType) + throw InspectionError.notSupported( + "Please use .find(ViewType.\(name).self) instead of .find(\(name).self) inspection call.") + } return try find(ViewType.View.self, relation: relation, where: condition) } /** Searches for a view of a specific type, which enclosed hierarchy contains a `Text` with the provided string - - Parameter inspectable: Your custom `Inspectable` view type. For example: `ContentView.self` + - Parameter customViewType: Your custom view type. For example: `ContentView.self` - Parameter containing: The string to look up for - Parameter locale: The locale for the text extraction. Defaults to `testsDefault` (i.e. `Locale(identifier: "en")`) - Throws: An error if the view cannot be found - Returns: A found view */ - func find(_ inspectable: V.Type, + func find(_ customViewType: V.Type, containing string: String, locale: Locale = .testsDefault - ) throws -> InspectableView> { + ) throws -> InspectableView> where V: SwiftUI.View { + guard !Inspector.isSystemType(type: customViewType) else { + let name = Inspector.typeName(type: customViewType) + throw InspectionError.notSupported( + "Please use .find(ViewType.\(name).self) instead of .find(\(name).self) inspection call.") + } return try find(ViewType.View.self, containing: string, locale: locale) } @@ -210,7 +220,7 @@ public extension InspectableView { func find(_ viewType: T.Type, containing string: String, locale: Locale = .testsDefault - ) throws -> InspectableView { + ) throws -> InspectableView where T: KnownViewType { return try find(ViewType.Text.self, where: { text in try text.string(locale: locale) == string && (try? text.find(T.self, relation: .parent)) != nil @@ -272,14 +282,14 @@ public extension InspectableView { The hierarchy is traversed in depth-first order, meaning that you'll get views ordered top-to-bottom as they appear in the code, regardless of their nesting depth. - - Parameter inspectable: Your custom `Inspectable` view type. For example: `ContentView.self` + - Parameter customViewType: Your custom view type. For example: `ContentView.self` - Parameter where: The condition closure for detecting a matching view. Thrown errors are interpreted as "this view does not match" - Returns: An array of all matching views or an empty array if none are found. */ - func findAll(_ inspectable: V.Type, + func findAll(_ customViewType: V.Type, where condition: (InspectableView>) throws -> Bool = { _ in true } - ) -> [InspectableView>] where V: Inspectable { + ) -> [InspectableView>] where V: SwiftUI.View { return findAll(ViewType.View.self, where: condition) } @@ -378,8 +388,8 @@ private extension UnwrappedView { if name.hasPrefix(ViewType.Popover.standardModifierName) { return "popover" } - if let inspectable = view as? Inspectable { - let missingObjects = inspectable.missingEnvironmentObjects + if !Inspector.isSystemType(value: view) { + let missingObjects = EnvironmentInjection.missingEnvironmentObjects(for: view) if missingObjects.count > 0 { return InspectionError .missingEnvironmentObjects(view: name, objects: missingObjects) diff --git a/Sources/ViewInspector/ViewSearchIndex.swift b/Sources/ViewInspector/ViewSearchIndex.swift index 879d1734..d4410d06 100644 --- a/Sources/ViewInspector/ViewSearchIndex.swift +++ b/Sources/ViewInspector/ViewSearchIndex.swift @@ -88,22 +88,24 @@ internal extension ViewSearch { 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) }) { + .first(where: { + $0.viewType.namespacedPrefixes.containsPrefixRegex(matching: fullName) + }) { return identity } - if (try? content.extractCustomView()) != nil, - let inspectable = content.view as? Inspectable { + if (try? content.extractCustomView()) != nil { let name = Inspector.typeName( value: content.view, generics: .customViewPlaceholder) - switch inspectable.entity { - case .view: + switch content.view { + case _ as any View: return .init(ViewType.View.self, genericTypeName: name) - case .viewModifier: + case _ as any ViewModifier: return .init(ViewType.ViewModifier.self, genericTypeName: name) - case .gesture: + case _ as any Gesture: + break + default: break } - } return nil } @@ -125,10 +127,7 @@ internal extension ViewSearch { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) internal extension ViewType { - struct Stub: Inspectable { - var entity: Content.InspectableEntity - func extractContent(environmentObjects: [AnyObject]) throws -> Any { () } - } + struct Stub { } } // MARK: - ViewIdentity diff --git a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureChangedTests.swift b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureChangedTests.swift index f51119a6..0c219550 100644 --- a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureChangedTests.swift +++ b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureChangedTests.swift @@ -8,7 +8,7 @@ import Combine @available(iOS 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class CommonComposedGestureChangedTests { +final class CommonComposedGestureChangedTests { let testCase: XCTestCase let type: U.Type @@ -63,7 +63,7 @@ final class CommonComposedGestureChangedTests { (_ChangedGesture<_EndedGesture>, _ChangedGesture<_EndedGesture>) -> T - func callChangedNotFirstTest( + func callChangedNotFirstTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureChangedNotFirst) throws { @@ -97,7 +97,7 @@ final class CommonComposedGestureChangedTests { (_ChangedGesture<_ChangedGesture>, _ChangedGesture<_ChangedGesture>) -> T - func callChangedMultipleTest( + func callChangedMultipleTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureChangedMultiple) throws { @@ -134,7 +134,7 @@ final class CommonComposedGestureChangedTests { testCase.wait(for: [exp1, exp2], timeout: 0.1) } - func callChangedFailureTest( + func callChangedFailureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { diff --git a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureEndedTests.swift b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureEndedTests.swift index b336743c..442c6553 100644 --- a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureEndedTests.swift +++ b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureEndedTests.swift @@ -8,7 +8,7 @@ import Combine @available(iOS 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class CommonComposedGestureEndedTests { +final class CommonComposedGestureEndedTests { let testCase: XCTestCase let type: U.Type @@ -31,7 +31,7 @@ final class CommonComposedGestureEndedTests { (_EndedGesture, _EndedGesture) -> T - func callEndedTest( + func callEndedTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureEnded) throws { @@ -63,7 +63,7 @@ final class CommonComposedGestureEndedTests { (_EndedGesture<_ChangedGesture>, _EndedGesture<_ChangedGesture>) -> T - func callEndedNotFirstTest( + func callEndedNotFirstTest ( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureEndedNotFirst) throws { @@ -97,7 +97,7 @@ final class CommonComposedGestureEndedTests { (_EndedGesture<_EndedGesture>, _EndedGesture<_EndedGesture>) -> T - func callEndedMultipleTest( + func callEndedMultipleTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureEndedMultiple) throws { @@ -134,7 +134,7 @@ final class CommonComposedGestureEndedTests { testCase.wait(for: [exp1, exp2], timeout: 0.1) } - func callEndedFailureTest( + func callEndedFailureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { diff --git a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureTests.swift b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureTests.swift index b527fd20..9ebfefac 100644 --- a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureTests.swift +++ b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureTests.swift @@ -8,7 +8,7 @@ import Combine @available(iOS 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class CommonComposedGestureTests { +final class CommonComposedGestureTests { let type: U.Type @@ -25,7 +25,7 @@ final class CommonComposedGestureTests { self.type = type } - func gestureTest( + func gestureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { @@ -41,7 +41,7 @@ final class CommonComposedGestureTests { } } - func gesturePathTest( + func gesturePathTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { @@ -57,7 +57,7 @@ final class CommonComposedGestureTests { } } - func gestureFailureTest( + func gestureFailureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { @@ -83,7 +83,7 @@ final class CommonComposedGestureTests { (_ModifiersGesture, _ModifiersGesture) -> T - func modifiersTest( + func modifiersTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureModifiers) throws { @@ -105,7 +105,7 @@ final class CommonComposedGestureTests { (_ModifiersGesture<_EndedGesture>, _ModifiersGesture<_EndedGesture>) -> T - func modifiersNotFirstTest( + func modifiersNotFirstTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureModifiersNotFirst) throws { @@ -131,7 +131,7 @@ final class CommonComposedGestureTests { (_ModifiersGesture<_ModifiersGesture>, _ModifiersGesture<_ModifiersGesture>) -> T - func modifiersMultipleTest( + func modifiersMultipleTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureModifiersMultiple) throws { @@ -153,7 +153,7 @@ final class CommonComposedGestureTests { } } - func modifiersFailureTest( + func modifiersFailureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { diff --git a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureUpdatingTests.swift b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureUpdatingTests.swift index de13e427..94612c88 100644 --- a/Tests/ViewInspectorTests/Gestures/CommonComposedGestureUpdatingTests.swift +++ b/Tests/ViewInspectorTests/Gestures/CommonComposedGestureUpdatingTests.swift @@ -8,7 +8,7 @@ import Combine @available(iOS 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class CommonComposedGestureUpdatingTests { +final class CommonComposedGestureUpdatingTests { @GestureState var gestureState = CGSize.zero @@ -33,7 +33,7 @@ final class CommonComposedGestureUpdatingTests { (GestureStateGesture, GestureStateGesture) -> T - func callUpdatingTest( + func callUpdatingTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureUpdating) throws { @@ -65,7 +65,7 @@ final class CommonComposedGestureUpdatingTests { (GestureStateGesture<_EndedGesture, CGSize>, GestureStateGesture<_EndedGesture, CGSize>) -> T - func callUpdatingNotFirstTest( + func callUpdatingNotFirstTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureUpdatingNotFirst) throws { @@ -99,7 +99,7 @@ final class CommonComposedGestureUpdatingTests { (GestureStateGesture, CGSize>, GestureStateGesture, CGSize>) -> T - func callUpdatingMultipleTest( + func callUpdatingMultipleTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: ComposedGestureUpdatingMultiple) throws { @@ -136,7 +136,7 @@ final class CommonComposedGestureUpdatingTests { testCase.wait(for: [exp1, exp2], timeout: 0.1) } - func callUpdatingFailureTest( + func callUpdatingFailureTest( _ order: InspectableView>.GestureOrder, file: StaticString = #filePath, line: UInt = #line, _ factory: (MagnificationGesture, RotationGesture) -> T) throws { diff --git a/Tests/ViewInspectorTests/Gestures/CommonGestureTests.swift b/Tests/ViewInspectorTests/Gestures/CommonGestureTests.swift index 1455aecc..43814976 100644 --- a/Tests/ViewInspectorTests/Gestures/CommonGestureTests.swift +++ b/Tests/ViewInspectorTests/Gestures/CommonGestureTests.swift @@ -6,7 +6,7 @@ import Combine // MARK: - Common Gesture Tests @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -final class CommonGestureTests { +final class CommonGestureTests { @GestureState var gestureState = CGSize.zero diff --git a/Tests/ViewInspectorTests/Gestures/ComposedGestureExampleTests.swift b/Tests/ViewInspectorTests/Gestures/ComposedGestureExampleTests.swift index 288dc91c..fd3aafe9 100644 --- a/Tests/ViewInspectorTests/Gestures/ComposedGestureExampleTests.swift +++ b/Tests/ViewInspectorTests/Gestures/ComposedGestureExampleTests.swift @@ -77,7 +77,7 @@ final class ComposedGestureExampleTests: XCTestCase { func testNotAComposedGestureError() throws { guard #available(iOS 14.0, tvOS 16.0, *) else { throw XCTSkip() } let sut = TestGestureView1() - let rectangle = try sut.inspect().shape(0) + let rectangle = try sut.inspect().shape() let tapGesture = try rectangle.gesture(TapGesture.self) XCTAssertThrows(try tapGesture.first(MagnificationGesture.self), "Type mismatch: TapGesture is not ExclusiveGesture, SequenceGesture, or SimultaneousGesture") @@ -107,7 +107,7 @@ final class ComposedGestureExampleTests: XCTestCase { @available(iOS 13.0, macOS 11, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct TestGestureView10: View & Inspectable { +struct TestGestureView10: View { @State var scale: CGFloat = 1.0 @State var angle = Angle(degrees: 0) @@ -140,7 +140,7 @@ struct TestGestureView10: View & Inspectable { @available(iOS 13.0, macOS 11, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct TestGestureView11: View & Inspectable { +struct TestGestureView11: View { @State var scale: CGFloat = 1.0 @State var angle = Angle(degrees: 0) @@ -172,7 +172,7 @@ struct TestGestureView11: View & Inspectable { @available(iOS 13.0, macOS 11, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct TestGestureView12: View & Inspectable { +struct TestGestureView12: View { internal let inspection = Inspection() internal let publisher = PassthroughSubject() diff --git a/Tests/ViewInspectorTests/Gestures/GestureExampleTests.swift b/Tests/ViewInspectorTests/Gestures/GestureExampleTests.swift index 8cb360c8..2227ace3 100644 --- a/Tests/ViewInspectorTests/Gestures/GestureExampleTests.swift +++ b/Tests/ViewInspectorTests/Gestures/GestureExampleTests.swift @@ -10,7 +10,7 @@ final class GestureExampleTests: XCTestCase { func testGestureModifier() throws { guard #available(iOS 14.0, tvOS 16.0, *) else { throw XCTSkip() } let sut = TestGestureView1() - let rectangle = try sut.inspect().shape(0) + let rectangle = try sut.inspect().shape() XCTAssertNoThrow(try rectangle.gesture(TapGesture.self)) } @@ -119,7 +119,7 @@ final class GestureExampleTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 16.0, *) -struct TestGestureView1: View & Inspectable { +struct TestGestureView1: View { @State var tapped = false var body: some View { @@ -134,7 +134,7 @@ struct TestGestureView1: View & Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 16.0, *) -struct TestGestureView2: View & Inspectable { +struct TestGestureView2: View { @State var tapped = false var body: some View { @@ -149,7 +149,7 @@ struct TestGestureView2: View & Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 16.0, *) -struct TestGestureView3: View & Inspectable { +struct TestGestureView3: View { @State var tapped = false var body: some View { @@ -165,7 +165,7 @@ struct TestGestureView3: View & Inspectable { @available(iOS 13.0, macOS 10.15, *) @available(tvOS, unavailable) -struct TestGestureView4: View & Inspectable { +struct TestGestureView4: View { @State var isDragging = false var body: some View { @@ -181,7 +181,7 @@ struct TestGestureView4: View & Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 14.0, *) -struct TestGestureView5: View & Inspectable { +struct TestGestureView5: View { @GestureState var isDetectingLongPress = false internal let inspection = Inspection() @@ -203,7 +203,7 @@ struct TestGestureView5: View & Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 14.0, *) -struct TestGestureView6: View & Inspectable { +struct TestGestureView6: View { @GestureState var isDetectingLongPress = false @State var totalNumberOfTaps = 0 @@ -237,7 +237,7 @@ struct TestGestureView6: View & Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 14.0, *) -struct TestGestureView7: View & Inspectable { +struct TestGestureView7: View { @GestureState var isDetectingLongPress = false @State var totalNumberOfTaps = 0 @State var doneCounting = false @@ -272,7 +272,7 @@ struct TestGestureView7: View & Inspectable { #if os(macOS) @available(macOS 10.15, *) -struct TestGestureView8: View & Inspectable { +struct TestGestureView8: View { @State var tapped = false var body: some View { @@ -290,7 +290,7 @@ struct TestGestureView8: View & Inspectable { #endif @available(iOS 14.0, macOS 10.15, tvOS 16.0, *) -struct TestGestureView9: View & Inspectable { +struct TestGestureView9: View { @State var tapped = false var body: some View { diff --git a/Tests/ViewInspectorTests/InspectableViewTests.swift b/Tests/ViewInspectorTests/InspectableViewTests.swift index 2afaac8f..b6e880ef 100644 --- a/Tests/ViewInspectorTests/InspectableViewTests.swift +++ b/Tests/ViewInspectorTests/InspectableViewTests.swift @@ -8,9 +8,6 @@ final class InspectableViewTests: XCTestCase { func testBasicInspectionFunctions() throws { let view = Text("abc") XCTAssertEqual(try view.inspect().text().string(), "abc") - view.inspect { view in - XCTAssertEqual(try view.text().string(), "abc") - } } func testIsResponsive() throws { @@ -58,6 +55,7 @@ final class InspectableViewTestsAccessTests: XCTestCase { func testCollectionWithAbsentViews() throws { let sut = try ViewWithAbsentChildren(present: false).inspect() + .view(ViewWithAbsentChildren.self) var counter = 0 // `forEach` is using iterator sut.forEach { _ in counter += 1 } @@ -75,7 +73,7 @@ final class InspectableViewTestsAccessTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ViewWithAbsentChildren: View, Inspectable { +private struct ViewWithAbsentChildren: View { let present: Bool @ViewBuilder diff --git a/Tests/ViewInspectorTests/InspectionEmissaryTests.swift b/Tests/ViewInspectorTests/InspectionEmissaryTests.swift index f8082c0e..c1689889 100644 --- a/Tests/ViewInspectorTests/InspectionEmissaryTests.swift +++ b/Tests/ViewInspectorTests/InspectionEmissaryTests.swift @@ -127,7 +127,7 @@ final class Inspection: InspectionEmissary { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestView: View, Inspectable { +private struct TestView: View { @State private(set) var flag: Bool let publisher = PassthroughSubject() @@ -150,7 +150,7 @@ private class ExternalState: ObservableObject { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestViewModifier: ViewModifier, Inspectable { +private struct TestViewModifier: ViewModifier { @Binding var flag: Bool @EnvironmentObject var envState: ExternalState diff --git a/Tests/ViewInspectorTests/InspectorTests.swift b/Tests/ViewInspectorTests/InspectorTests.swift index 347e6fd2..0738fc79 100644 --- a/Tests/ViewInspectorTests/InspectorTests.swift +++ b/Tests/ViewInspectorTests/InspectorTests.swift @@ -237,7 +237,7 @@ final class InspectableViewModifiersTests: XCTestCase { XCTAssertEqual(sut2.pathToRoot, "emptyView()") let view2 = TestPrintView() let sut3 = try view2.inspect() - XCTAssertEqual(sut3.pathToRoot, "view(TestPrintView.self)") + XCTAssertEqual(sut3.pathToRoot, "") let sut4 = try view2.inspect().text() XCTAssertEqual(sut4.pathToRoot, "view(TestPrintView.self).text()") let sut5 = try view2.inspect().text(0) @@ -292,7 +292,7 @@ private struct Struct1 { private struct Struct3 { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestPrintView: View, Inspectable { +private struct TestPrintView: View { let str = ["abc", "def"] diff --git a/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift b/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift index e0982356..61bcd1e3 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ActionSheetTests.swift @@ -257,7 +257,7 @@ private struct InspectableActionSheetWithItem: ViewModifier, } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ActionSheetFindTestView: View, Inspectable { +private struct ActionSheetFindTestView: View { @Binding var isSheet1Presented = false @Binding var isSheet2Presented = false @@ -289,7 +289,7 @@ private struct ActionSheetFindTestView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct PopupMixTestView: View, Inspectable { +private struct PopupMixTestView: View { @Binding var isAlertPresented = true @Binding var isActionSheetPresented = true diff --git a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift index 0e6f7e84..283891c6 100644 --- a/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/AlertTests.swift @@ -333,7 +333,7 @@ private struct InspectableAlertWithItem: ViewModifier, ItemP } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct AlertFindTestView: View, Inspectable { +private struct AlertFindTestView: View { @Binding var isAlert1Presented = false @Binding var isAlert2Presented = false diff --git a/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift b/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift index 6a84ae7a..ae568d06 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ButtonTests.swift @@ -204,6 +204,3 @@ private struct TestPrimitiveButtonStyle: PrimitiveButtonStyle { #endif } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension TestPrimitiveButtonStyle.TestButton: Inspectable { } diff --git a/Tests/ViewInspectorTests/SwiftUI/ConditionalContentTests.swift b/Tests/ViewInspectorTests/SwiftUI/ConditionalContentTests.swift index f6869c51..44a31b26 100644 --- a/Tests/ViewInspectorTests/SwiftUI/ConditionalContentTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/ConditionalContentTests.swift @@ -31,7 +31,7 @@ final class ConditionalContentTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ConditionalView: View, Inspectable { +private struct ConditionalView: View { @ObservedObject var viewModel = ViewModel() var body: some View { @@ -47,7 +47,7 @@ private struct ConditionalView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ConditionalViewWithModifier: View, Inspectable { +private struct ConditionalViewWithModifier: View { let value: Bool diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift index 1325fd27..128b94e2 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewBuilderTests.swift @@ -71,7 +71,7 @@ final class CustomViewBuilderTests: XCTestCase { func testActualView() throws { let sut = TestViewBuilderView { Text("Test") } - XCTAssertNoThrow(try sut.inspect().actualView().content) + XCTAssertNoThrow(try sut.inspect().view(TestViewBuilderView.self).actualView().content) } func testViewBody() { @@ -105,6 +105,3 @@ private struct TestViewBuilderView: View { .padding() } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension TestViewBuilderView: Inspectable { } diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewModifierTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewModifierTests.swift index c7dec129..3388c5e7 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewModifierTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewModifierTests.swift @@ -34,12 +34,6 @@ final class ModifiedContentTests: XCTestCase { XCTAssertEqual(try view.inspect().hStack().text(1).content.medium.viewModifiers.count, 2) } - func testNonInspectableModifier() throws { - let sut = EmptyView().modifier(NonInspectableModifier()) - XCTAssertThrows(try sut.inspect().find(ViewType.ViewModifierContent.self), - "Search did not find a match") - } - func testNoModifiersError() throws { let sut = EmptyView().padding() XCTAssertThrows(try sut.inspect().emptyView().modifier(TestModifier.self), @@ -153,14 +147,7 @@ final class ModifiedContentTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct NonInspectableModifier: ViewModifier { - func body(content: Self.Content) -> some View { - content - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestModifier: ViewModifier, Inspectable { +private struct TestModifier: ViewModifier { var tag: Int = 0 func body(content: Self.Content) -> some View { content.onAppear(perform: { }) @@ -168,7 +155,7 @@ private struct TestModifier: ViewModifier, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestModifier2: ViewModifier, Inspectable { +private struct TestModifier2: ViewModifier { @Binding var value: Bool var didAppear: ((Self) -> Void)? @@ -184,7 +171,7 @@ private struct TestModifier2: ViewModifier, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestModifier3: ViewModifier, Inspectable { +private struct TestModifier3: ViewModifier { @EnvironmentObject var viewModel: ExternalState @@ -202,7 +189,7 @@ private class ExternalState: ObservableObject { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestModifier4: ViewModifier, Inspectable { +private struct TestModifier4: ViewModifier { let injection: ExternalState @@ -226,7 +213,7 @@ private struct TestModifier4: ViewModifier, Inspectable { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) private extension TestModifier4 { - struct ViewWithEnvObject: View, Inspectable { + struct ViewWithEnvObject: View { @EnvironmentObject var envObj: ExternalState diff --git a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift index c042c351..41156bd5 100644 --- a/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/CustomViewTests.swift @@ -63,6 +63,44 @@ final class CustomViewTests: XCTestCase { XCTAssertEqual(sut.content.medium.viewModifiers.count, 0) } + func testImplicitChildUnwrapping() throws { + let sut = SimpleTestView() + let implicit = try sut.inspect() + .emptyView().pathToRoot + let explicit = try sut.inspect() + .view(SimpleTestView.self) + .emptyView().pathToRoot + XCTAssertEqual(implicit, "view(SimpleTestView.self).emptyView()") + XCTAssertEqual(explicit, "view(SimpleTestView.self).emptyView()") + } + + func testCustomViewAPIMisuseError() throws { + let sut = try SimpleTestView().inspect().view(SimpleTestView.self) + XCTAssertNoThrow(try sut.emptyView()) + XCTAssertNoThrow(try sut.emptyView(0)) + XCTAssertNoThrow(try sut.find(ViewType.EmptyView.self)) + XCTAssertThrows(try sut.find(EmptyView.self), + """ + Please use .find(ViewType.EmptyView.self) instead \ + of .find(EmptyView.self) inspection call. + """) + XCTAssertThrows(try sut.find(EmptyView.self, containing: "abc"), + """ + Please use .find(ViewType.EmptyView.self) instead \ + of .find(EmptyView.self) inspection call. + """) + XCTAssertThrows(try sut.view(EmptyView.self), + """ + Please replace .view(EmptyView.self) inspection call \ + with .emptyView() or .find(ViewType.EmptyView.self) + """) + XCTAssertThrows(try sut.view(EmptyView.self, 0), + """ + Please replace .view(EmptyView.self, 0) inspection \ + call with .emptyView(0) + """) + } + func testEnvViewResetsModifiers() throws { let sut = EnvironmentStateTestView() let exp = sut.inspection.inspect { view in @@ -165,13 +203,6 @@ final class CustomViewTests: XCTestCase { wait(for: [exp], timeout: 0.1) } - func testSearchBlocker() throws { - let sut = AnyView(NonInspectableTestView()) - XCTAssertThrows(try sut.inspect().find(ViewType.EmptyView.self), - "Search did not find a match. Possible blockers: NonInspectableTestView") - XCTAssertEqual(try sut.inspect().findAll(ViewType.EmptyView.self).count, 0) - } - func testActualView() throws { let sut = LocalStateTestView(flag: true) let exp = sut.inspection.inspect { view in @@ -229,7 +260,6 @@ final class CustomViewTests: XCTestCase { } func testTestViews() { - XCTAssertNoThrow(NonInspectableTestView().body) XCTAssertNoThrow(SimpleTestView().body) XCTAssertNoThrow(ObservedStateTestView(viewModel: ExternalState()).body) } @@ -238,21 +268,14 @@ final class CustomViewTests: XCTestCase { // MARK: - Test Views @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct NonInspectableTestView: View { - var body: some View { - EmptyView() - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct SimpleTestView: View, Inspectable { +private struct SimpleTestView: View { var body: some View { EmptyView() } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct LocalStateTestView: View, Inspectable { +private struct LocalStateTestView: View { @State private(set) var flag: Bool let inspection = Inspection() @@ -266,7 +289,7 @@ private struct LocalStateTestView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ObservedStateTestView: View, Inspectable { +private struct ObservedStateTestView: View { @ObservedObject var viewModel: ExternalState @@ -276,7 +299,7 @@ private struct ObservedStateTestView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct EnvironmentStateTestView: View, Inspectable { +private struct EnvironmentStateTestView: View { @EnvironmentObject var viewModel: ExternalState let inspection = Inspection() @@ -288,7 +311,7 @@ private struct EnvironmentStateTestView: View, Inspectable { } #if os(macOS) -private struct TestViewRepresentable: NSViewRepresentable, Inspectable { +private struct TestViewRepresentable: NSViewRepresentable { func makeNSView(context: NSViewRepresentableContext) -> NSView { let view = NSView() @@ -301,7 +324,7 @@ private struct TestViewRepresentable: NSViewRepresentable, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestViewControllerRepresentable: NSViewControllerRepresentable, Inspectable { +private struct TestViewControllerRepresentable: NSViewControllerRepresentable { func makeNSViewController(context: Context) -> NSViewController { let vc = NSViewController() @@ -314,7 +337,7 @@ private struct TestViewControllerRepresentable: NSViewControllerRepresentable, I } #elseif os(iOS) || os(tvOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestViewRepresentable: UIViewRepresentable, Inspectable { +private struct TestViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> UIView { let view = UIView() @@ -327,7 +350,7 @@ private struct TestViewRepresentable: UIViewRepresentable, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestViewControllerRepresentable: UIViewControllerRepresentable, Inspectable { +private struct TestViewControllerRepresentable: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { let vc = UIViewController() @@ -341,7 +364,7 @@ private struct TestViewControllerRepresentable: UIViewControllerRepresentable, I #elseif os(watchOS) @available(watchOS, deprecated: 7.0) -private struct TestViewRepresentable: WKInterfaceObjectRepresentable, Inspectable { +private struct TestViewRepresentable: WKInterfaceObjectRepresentable { typealias Context = WKInterfaceObjectRepresentableContext func makeWKInterfaceObject(context: Context) -> some WKInterfaceObject { @@ -384,28 +407,28 @@ extension EnvironmentValues { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension ViewType { - struct Test: KnownViewType, CustomViewType where T: Inspectable { + struct Test: KnownViewType, CustomViewType { public static var typePrefix: String { "String" } static var namespacedPrefixes: [String] { ["Swift.String"] } } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct NameMatchView: View, Inspectable { +private struct NameMatchView: View { var body: some View { Text("") } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct NameMatchViewList: View, Inspectable { +private struct NameMatchViewList: View { var body: some View { ForEach(0..<5) { _ in NameMatchView() } } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct GenericContainer: View, Inspectable { +private struct GenericContainer: View { var body: some View { TestView() } @@ -413,7 +436,7 @@ private struct GenericContainer: View, Inspectable { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) private extension GenericContainer { - struct TestView: View, Inspectable { + struct TestView: View { var body: some View { Text("") } diff --git a/Tests/ViewInspectorTests/SwiftUI/DisclosureGroupTests.swift b/Tests/ViewInspectorTests/SwiftUI/DisclosureGroupTests.swift index f0e85dce..df6e0b5c 100644 --- a/Tests/ViewInspectorTests/SwiftUI/DisclosureGroupTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/DisclosureGroupTests.swift @@ -102,7 +102,7 @@ final class DisclosureGroupTests: XCTestCase { @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -private struct TestViewState: View, Inspectable { +private struct TestViewState: View { @ObservedObject var state = ExpansionState() var body: some View { @@ -120,7 +120,7 @@ private struct TestViewState: View, Inspectable { @available(iOS 14.0, macOS 11.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -private struct TestViewBinding: View, Inspectable { +private struct TestViewBinding: View { @Binding var expanded: Bool = false diff --git a/Tests/ViewInspectorTests/SwiftUI/EnvironmentObjectInjectionTests.swift b/Tests/ViewInspectorTests/SwiftUI/EnvironmentObjectInjectionTests.swift index 6996a575..df45296b 100644 --- a/Tests/ViewInspectorTests/SwiftUI/EnvironmentObjectInjectionTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/EnvironmentObjectInjectionTests.swift @@ -32,8 +32,8 @@ class EnvironmentObjectInjectionTests: XCTestCase { let obj1 = TestEnvObject1() let obj2 = TestEnvObject2() var sut = EnvironmentObjectInnerView() - sut.inject(environmentObject: obj1) - sut.inject(environmentObject: obj2) + sut = EnvironmentInjection.inject(environmentObject: obj1, into: sut) + sut = EnvironmentInjection.inject(environmentObject: obj2, into: sut) XCTAssertEqual(try sut.inspect().find(ViewType.Text.self).string(), "env_true") } @@ -87,7 +87,7 @@ class EnvironmentObjectInjectionTests: XCTestCase { Search did not find a match. Possible blockers: EnvironmentObjectInnerView is \ missing EnvironmentObjects: [\"obj2: TestEnvObject2\", \"obj1: TestEnvObject1\"] """) - sut.inject(environmentObject: TestEnvObject1()) + sut = EnvironmentInjection.inject(environmentObject: TestEnvObject1(), into: sut) XCTAssertThrows(try sut.inspect().find(ViewType.Text.self), """ Search did not find a match. Possible blockers: EnvironmentObjectInnerView is \ @@ -109,7 +109,7 @@ private class TestEnvObject2: ObservableObject { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct EnvironmentObjectInnerView: View, Inspectable { +private struct EnvironmentObjectInnerView: View { private var iVar1: Int8 = 0 private var iVar2: Int16 = 0 @@ -128,7 +128,7 @@ private struct EnvironmentObjectInnerView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct EnvironmentObjectOuterView: View, Inspectable { +private struct EnvironmentObjectOuterView: View { private var iVar1: Bool = false @EnvironmentObject var obj1: TestEnvObject1 @@ -146,7 +146,7 @@ private struct EnvironmentObjectOuterView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct EnvironmentObjectViewModifier: ViewModifier, Inspectable { +private struct EnvironmentObjectViewModifier: ViewModifier { private var iVar1: Bool = false @EnvironmentObject var obj2: TestEnvObject2 @EnvironmentObject var obj1: TestEnvObject1 diff --git a/Tests/ViewInspectorTests/SwiftUI/EquatableViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/EquatableViewTests.swift index 52857f99..d8abfda7 100644 --- a/Tests/ViewInspectorTests/SwiftUI/EquatableViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/EquatableViewTests.swift @@ -45,7 +45,7 @@ final class GlobalModifiersForEquatableView: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestView: View, Equatable, Inspectable { +private struct TestView: View, Equatable { var body: some View { EmptyView() } static func == (lhs: Self, rhs: Self) -> Bool { true } diff --git a/Tests/ViewInspectorTests/SwiftUI/FullScreenCoverTests.swift b/Tests/ViewInspectorTests/SwiftUI/FullScreenCoverTests.swift index 042e2a75..c45f7352 100644 --- a/Tests/ViewInspectorTests/SwiftUI/FullScreenCoverTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/FullScreenCoverTests.swift @@ -213,7 +213,7 @@ where Item: Identifiable, FullScreenCover: View { @available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) @available(macOS, unavailable) -private struct FullScreenCoverFindTestView: View, Inspectable { +private struct FullScreenCoverFindTestView: View { @Binding var isFullScreenCover1Presented = false @Binding var isFullScreenCover2Presented = false diff --git a/Tests/ViewInspectorTests/SwiftUI/NavigationLinkTests.swift b/Tests/ViewInspectorTests/SwiftUI/NavigationLinkTests.swift index 5d249c94..17564a38 100644 --- a/Tests/ViewInspectorTests/SwiftUI/NavigationLinkTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/NavigationLinkTests.swift @@ -176,7 +176,7 @@ final class NavigationLinkTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestView: View, Inspectable { +private struct TestView: View { let parameter: String var body: some View { @@ -185,7 +185,7 @@ private struct TestView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) -private struct TestViewState: View, Inspectable { +private struct TestViewState: View { @ObservedObject var state = NavigationState() var tag1: String { "tag1" } @@ -202,7 +202,7 @@ private struct TestViewState: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) -private struct TestViewBinding: View, Inspectable { +private struct TestViewBinding: View { @Binding var selection: String? diff --git a/Tests/ViewInspectorTests/SwiftUI/OpaqueViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/OpaqueViewTests.swift index 91962c6f..01f6adaa 100644 --- a/Tests/ViewInspectorTests/SwiftUI/OpaqueViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/OpaqueViewTests.swift @@ -35,14 +35,14 @@ final class OpaqueViewTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct InspectableTestView: View, Inspectable { +private struct InspectableTestView: View { var body: some View { Text("Test") } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct EnvInspectableTestView: View, Inspectable { +private struct EnvInspectableTestView: View { var body: some View { body(State()) diff --git a/Tests/ViewInspectorTests/SwiftUI/OptionalViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/OptionalViewTests.swift index 1c5f6548..a8607f84 100644 --- a/Tests/ViewInspectorTests/SwiftUI/OptionalViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/OptionalViewTests.swift @@ -57,7 +57,7 @@ final class OptionalViewTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct OptionalView: View, Inspectable { +private struct OptionalView: View { let flag: Bool var body: some View { @@ -68,7 +68,7 @@ private struct OptionalView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct MixedOptionalView: View, Inspectable { +private struct MixedOptionalView: View { let flag: Bool var body: some View { diff --git a/Tests/ViewInspectorTests/SwiftUI/PopoverTests.swift b/Tests/ViewInspectorTests/SwiftUI/PopoverTests.swift index f13a9e0d..1199d4ca 100644 --- a/Tests/ViewInspectorTests/SwiftUI/PopoverTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/PopoverTests.swift @@ -24,7 +24,6 @@ final class PopoverTests: XCTestCase { func testInspectionErrorCustomModifierRequired() throws { let binding = Binding(wrappedValue: true) let sut = EmptyView().popover(isPresented: binding) { Text("") } - print("\(Inspector.print(sut) as AnyObject)") XCTAssertThrows(try sut.inspect().emptyView().popover(), """ Please refer to the Guide for inspecting the Popover: \ @@ -253,7 +252,7 @@ where Item: Identifiable, Popover: View { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct PopoverFindTestView: View, Inspectable { +private struct PopoverFindTestView: View { @Binding var isPopover1Presented = false @Binding var isPopover2Presented = false diff --git a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift index 07d007c4..4d5103e8 100644 --- a/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/PreferenceTests.swift @@ -109,7 +109,7 @@ final class PreferenceTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ManyOverlaysView: View, Inspectable { +private struct ManyOverlaysView: View { var body: some View { EmptyView() .overlay(AnyView(Text("Test"))) @@ -122,7 +122,7 @@ private struct ManyOverlaysView: View, Inspectable { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -private struct ManyOverlaysViewIOS15: View, Inspectable { +private struct ManyOverlaysViewIOS15: View { var body: some View { EmptyView() .overlay(Spacer()) @@ -135,7 +135,7 @@ private struct ManyOverlaysViewIOS15: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct ManyBGOverlaysView: View, Inspectable { +private struct ManyBGOverlaysView: View { var body: some View { EmptyView() .background(AnyView(Text("Test"))) diff --git a/Tests/ViewInspectorTests/SwiftUI/SheetTests.swift b/Tests/ViewInspectorTests/SwiftUI/SheetTests.swift index d850453d..3f63007c 100644 --- a/Tests/ViewInspectorTests/SwiftUI/SheetTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/SheetTests.swift @@ -176,7 +176,7 @@ where Item: Identifiable, Sheet: View { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct SheetFindTestView: View, Inspectable { +private struct SheetFindTestView: View { @Binding var isSheet1Presented = false @Binding var isSheet2Presented = false diff --git a/Tests/ViewInspectorTests/SwiftUI/SubscriptionViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/SubscriptionViewTests.swift index 9fe9af8a..3130b664 100644 --- a/Tests/ViewInspectorTests/SwiftUI/SubscriptionViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/SubscriptionViewTests.swift @@ -25,7 +25,7 @@ final class SubscriptionViewTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct SubscriptionTestView: View, Inspectable { +private struct SubscriptionTestView: View { let publisher: AnyPublisher diff --git a/Tests/ViewInspectorTests/SwiftUI/TextTests.swift b/Tests/ViewInspectorTests/SwiftUI/TextTests.swift index 23dd84c8..fde50484 100644 --- a/Tests/ViewInspectorTests/SwiftUI/TextTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/TextTests.swift @@ -204,6 +204,33 @@ final class TextTests: XCTestCase { let sut3 = Text("abc") XCTAssertEqual(try sut3.inspect().text().images(), []) } + + func testAttributedStringTextStorage() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + else { throw XCTSkip() } + let aString = try AttributedString(markdown: "**Bold** Test [Link](https://example.com)") + let sut = Text("head ") + Text(aString) + Text(" tail") + XCTAssertEqual(try sut.inspect().text().string(), "head Bold Test Link tail") + } + + // MARK: - attributedString() + + func testAttributedStringValue() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + else { throw XCTSkip() } + let aString = try AttributedString(markdown: "**Bold** Test [Link](https://example.com)") + let sut = Text(aString) + let value = try sut.inspect().text().attributedString() + XCTAssertEqual(value, aString) + } + + func testAttributedStringFoundString() throws { + guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + else { throw XCTSkip() } + let sut = Text("Test") + XCTAssertThrows(try sut.inspect().text().attributedString(), + "Please use attributes() for accessing the text styles on this view") + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) diff --git a/Tests/ViewInspectorTests/SwiftUI/TupleViewTests.swift b/Tests/ViewInspectorTests/SwiftUI/TupleViewTests.swift index 1020cc37..27aa95d3 100644 --- a/Tests/ViewInspectorTests/SwiftUI/TupleViewTests.swift +++ b/Tests/ViewInspectorTests/SwiftUI/TupleViewTests.swift @@ -49,7 +49,7 @@ final class TupleViewTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct SimpleTupleView: View, Inspectable { +private struct SimpleTupleView: View { var body: some View { EmptyView() Text("abc") @@ -57,7 +57,7 @@ private struct SimpleTupleView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TupleInsideTupleView: View, Inspectable { +private struct TupleInsideTupleView: View { let flag: Bool var body: some View { diff --git a/Tests/ViewInspectorTests/ViewHostingTests.swift b/Tests/ViewInspectorTests/ViewHostingTests.swift index a23717e4..db0395f3 100644 --- a/Tests/ViewInspectorTests/ViewHostingTests.swift +++ b/Tests/ViewInspectorTests/ViewHostingTests.swift @@ -194,7 +194,7 @@ private class NSViewWithTag: NSView { static let onTag: Int = 43 } -private struct NSTestView: NSViewRepresentable, Inspectable { +private struct NSTestView: NSViewRepresentable { typealias UpdateContext = NSViewRepresentableContext @@ -212,7 +212,7 @@ private struct NSTestView: NSViewRepresentable, Inspectable { } extension NSTestView { - struct WrapperView: View, Inspectable { + struct WrapperView: View { @State var flag: Bool var didAppear: ((Self) -> Void)? @@ -227,7 +227,7 @@ extension NSTestView { #elseif os(iOS) || os(tvOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct UITestView: UIViewRepresentable, Inspectable { +private struct UITestView: UIViewRepresentable { typealias UpdateContext = UIViewRepresentableContext @@ -249,7 +249,7 @@ private struct UITestView: UIViewRepresentable, Inspectable { @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) extension UITestView { - struct WrapperView: View, Inspectable { + struct WrapperView: View { @State var flag: Bool var didAppear: ((Self) -> Void)? @@ -265,7 +265,7 @@ extension UITestView { @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) @available(watchOS, deprecated: 7.0) -private struct WKTestView: WKInterfaceObjectRepresentable, Inspectable { +private struct WKTestView: WKInterfaceObjectRepresentable { var didUpdate: () -> Void @@ -282,7 +282,7 @@ private struct WKTestView: WKInterfaceObjectRepresentable, Inspectable { @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) @available(watchOS, deprecated: 7.0) extension WKTestView { - struct WrapperView: View, Inspectable { + struct WrapperView: View { var didAppear: ((Self) -> Void)? var didUpdate: () -> Void @@ -296,7 +296,7 @@ extension WKTestView { #endif #if os(macOS) -private struct NSTestVC: NSViewControllerRepresentable, Inspectable { +private struct NSTestVC: NSViewControllerRepresentable { class TestVC: NSViewController { override func loadView() { @@ -325,7 +325,7 @@ private struct NSTestVC: NSViewControllerRepresentable, Inspectable { } #elseif os(iOS) || os(tvOS) @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct UITestVC: UIViewControllerRepresentable, Inspectable { +private struct UITestVC: UIViewControllerRepresentable { class TestVC: UIViewController { } diff --git a/Tests/ViewInspectorTests/ViewModifiers/CustomStyleModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/CustomStyleModifiersTests.swift index e4543e9c..d8eaae6f 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/CustomStyleModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/CustomStyleModifiersTests.swift @@ -145,6 +145,3 @@ extension RedOutlineHelloWorldStyle { return try makeBody(configuration: configuration).inspect() } } - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -extension RedOutlineHelloWorldStyle.StyleBody: Inspectable {} diff --git a/Tests/ViewInspectorTests/ViewModifiers/NavigationBarModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/NavigationBarModifiersTests.swift index 06202e1c..21b22c28 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/NavigationBarModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/NavigationBarModifiersTests.swift @@ -158,7 +158,7 @@ final class NavigationBarItemsTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestView: View, Inspectable { +private struct TestView: View { let inspection = Inspection() diff --git a/Tests/ViewInspectorTests/ViewModifiers/TransitiveModifiersTests.swift b/Tests/ViewInspectorTests/ViewModifiers/TransitiveModifiersTests.swift index 628846f3..c4005fa1 100644 --- a/Tests/ViewInspectorTests/ViewModifiers/TransitiveModifiersTests.swift +++ b/Tests/ViewInspectorTests/ViewModifiers/TransitiveModifiersTests.swift @@ -2,7 +2,7 @@ import XCTest import SwiftUI @testable import ViewInspector -@available(iOS 13.0, macOS 10.15, tvOS 13.0, *) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 9.0, *) final class TransitiveModifiersTests: XCTestCase { func testHiddenTransitivity() throws { @@ -23,7 +23,6 @@ final class TransitiveModifiersTests: XCTestCase { } @available(tvOS, unavailable) - @available(watchOS, unavailable) func testFlipsRightToLeftInheritance() throws { let sut = try FlipsRightToLeftTestView().inspect() if #available(iOS 14.0, tvOS 14.0, *) { @@ -79,7 +78,7 @@ final class TransitiveModifiersTests: XCTestCase { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct HittenTestView: View, Inspectable { +private struct HittenTestView: View { var body: some View { VStack { Button("abc", action: { }) @@ -91,7 +90,7 @@ private struct HittenTestView: View, Inspectable { } @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) -private struct TestDisabledView: View, Inspectable { +private struct TestDisabledView: View { var body: some View { VStack { Button(action: { }, label: { @@ -111,10 +110,9 @@ private struct TestDisabledView: View, Inspectable { } } -@available(iOS 13.0, macOS 10.15, *) +@available(iOS 13.0, macOS 10.15, watchOS 9.0, *) @available(tvOS, unavailable) -@available(watchOS, unavailable) -private struct FlipsRightToLeftTestView: View, Inspectable { +private struct FlipsRightToLeftTestView: View { var body: some View { VStack { Stepper("1", onIncrement: nil, onDecrement: nil) @@ -127,7 +125,7 @@ private struct FlipsRightToLeftTestView: View, Inspectable { } @available(iOS 13.0, macOS 11.0, tvOS 13.0, *) -private struct ColorSchemeTestView: View, Inspectable { +private struct ColorSchemeTestView: View { var body: some View { VStack { Text("1") @@ -145,7 +143,7 @@ private struct ColorSchemeTestView: View, Inspectable { } @available(iOS 13.0, macOS 11.0, tvOS 13.0, *) -private struct AllowsHitTestingTestView: View, Inspectable { +private struct AllowsHitTestingTestView: View { var body: some View { VStack { @@ -161,10 +159,9 @@ private struct AllowsHitTestingTestView: View, Inspectable { } } -@available(iOS 13.0, macOS 10.15, *) +@available(iOS 13.0, macOS 10.15, watchOS 9.0, *) @available(tvOS, unavailable) -@available(watchOS, unavailable) -private struct TestLabelsHiddenView: View, Inspectable { +private struct TestLabelsHiddenView: View { var body: some View { VStack { Stepper(onIncrement: nil, onDecrement: nil, label: { diff --git a/Tests/ViewInspectorTests/ViewSearchTests.swift b/Tests/ViewInspectorTests/ViewSearchTests.swift index 1f219539..6dd75108 100644 --- a/Tests/ViewInspectorTests/ViewSearchTests.swift +++ b/Tests/ViewInspectorTests/ViewSearchTests.swift @@ -6,7 +6,7 @@ import SwiftUI @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) private struct Test { - struct InnerView: View, Inspectable { + struct InnerView: View { var body: some View { Button(action: { }, label: { HStack { Text("Btn") } @@ -15,7 +15,7 @@ private struct Test { }) } } - struct MainView: View, Inspectable { + struct MainView: View { var body: some View { AnyView(Group { SwiftUI.EmptyView() @@ -39,7 +39,7 @@ private struct Test { }) } } - struct ConditionalView: View, Inspectable { + struct ConditionalView: View { let falseCondition = false let trueCondition = true @@ -55,18 +55,13 @@ private struct Test { } } } - struct Modifier: ViewModifier, Inspectable { + struct Modifier: ViewModifier { let text: String func body(content: Modifier.Content) -> some View { AnyView(content.overlay(Text(text))) } } - struct NonInspectableView: View { - var body: some View { - SwiftUI.EmptyView() - } - } - struct EmptyView: View, Inspectable { + struct EmptyView: View { var body: some View { Text("empty") } @@ -214,16 +209,6 @@ final class ViewSearchTests: XCTestCase { XCTAssertEqual(values, ["2", "3"]) } - func testFindMatchingBlockerView() { - let view = AnyView(Test.NonInspectableView().id(5)) - XCTAssertNoThrow(try view.inspect().find(viewWithId: 5)) - let err = "Search did not find a match. Possible blockers: NonInspectableView" - XCTAssertThrows(try view.inspect().find(ViewType.EmptyView.self, - traversal: .breadthFirst), err) - XCTAssertThrows(try view.inspect().find(ViewType.EmptyView.self, - traversal: .depthFirst), err) - } - func testConflictingViewTypeNames() throws { guard #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) else { throw XCTSkip() } @@ -256,7 +241,7 @@ final class ViewSearchTests: XCTestCase { @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) private extension Test { - struct AccessibleView: View, Inspectable { + struct AccessibleView: View { var body: some View { Button(action: { }, label: { HStack { diff --git a/ViewInspector.podspec b/ViewInspector.podspec index 724f9cd2..71187bb6 100644 --- a/ViewInspector.podspec +++ b/ViewInspector.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.osx.deployment_target = '10.15' s.tvos.deployment_target = '13.0' #s.watchos.deployment_target = '7.0' - s.swift_version = '5.0' + s.swift_version = '5.7' s.framework = 'XCTest' s.source = { :git => "https://github.com/nalexn/ViewInspector.git", :tag => "#{s.version}" } diff --git a/ViewInspector.xcodeproj/project.pbxproj b/ViewInspector.xcodeproj/project.pbxproj index 85798bd0..2c939f53 100644 --- a/ViewInspector.xcodeproj/project.pbxproj +++ b/ViewInspector.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 522090522776401C001A899A /* LocationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522090512776401C001A899A /* LocationButton.swift */; }; 5223540026D62CB2008DA52F /* ToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522353FF26D62CB2008DA52F /* ToolbarTests.swift */; }; 5223540226D63B7A008DA52F /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5223540126D63B79008DA52F /* Toolbar.swift */; }; + 52277BE4295446490077D85D /* ContentExtraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52277BE3295446480077D85D /* ContentExtraction.swift */; }; 5236C34326CDC2F4007432E1 /* UnaryViewAdaptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5236C34226CDC2F4007432E1 /* UnaryViewAdaptor.swift */; }; 523CD4542777568C00D4FD3A /* SignInWithAppleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523CD4532777568C00D4FD3A /* SignInWithAppleButton.swift */; }; 523CD456277756A800D4FD3A /* SignInWithAppleButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523CD455277756A800D4FD3A /* SignInWithAppleButtonTests.swift */; }; @@ -309,6 +310,7 @@ 522090512776401C001A899A /* LocationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationButton.swift; sourceTree = ""; }; 522353FF26D62CB2008DA52F /* ToolbarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarTests.swift; sourceTree = ""; }; 5223540126D63B79008DA52F /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; + 52277BE3295446480077D85D /* ContentExtraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentExtraction.swift; sourceTree = ""; }; 5236C34226CDC2F4007432E1 /* UnaryViewAdaptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnaryViewAdaptor.swift; sourceTree = ""; }; 523CD4532777568C00D4FD3A /* SignInWithAppleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithAppleButton.swift; sourceTree = ""; }; 523CD455277756A800D4FD3A /* SignInWithAppleButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithAppleButtonTests.swift; sourceTree = ""; }; @@ -617,6 +619,7 @@ isa = PBXGroup; children = ( F60EEBBB2382EED0007DB53A /* BaseTypes.swift */, + 52277BE3295446480077D85D /* ContentExtraction.swift */, F6A35A3F25B35E920068B8B2 /* LazyGroup.swift */, F60EEBBA2382EED0007DB53A /* Inspector.swift */, F60EEBBC2382EED0007DB53A /* InspectableView.swift */, @@ -1148,6 +1151,7 @@ F6D933A72385E9E000358E0E /* EquatableView.swift in Sources */, 52F356AA267692D100695E43 /* MapAnnotation.swift in Sources */, F68108A623A5835E00B32145 /* EventsModifiers.swift in Sources */, + 52277BE4295446490077D85D /* ContentExtraction.swift in Sources */, F6D933AF2385EBD900358E0E /* HSplitView.swift in Sources */, F60EEBD52382EED0007DB53A /* AnyView.swift in Sources */, F6F08E6623A29FE4001F04DF /* SubscriptionView.swift in Sources */, diff --git a/guide.md b/guide.md index bf8bc8c3..cd0134cf 100644 --- a/guide.md +++ b/guide.md @@ -27,13 +27,11 @@ import XCTest import ViewInspector // 1. @testable import MyApp -extension ContentView: Inspectable { } // 2. - final class ContentViewTests: XCTestCase { - func testStringValue() throws { // 3. + func testStringValue() throws { // 2. let sut = ContentView() - let value = try sut.inspect().text().string() // 4. + let value = try sut.inspect().text().string() // 3. XCTAssertEqual(value, "Hello, world!") } } @@ -41,9 +39,8 @@ final class ContentViewTests: XCTestCase { So, you need to do the following: 1. Add `import ViewInspector` -2. Extend your view to conform to `Inspectable` in the test target scope. -3. Annotate the test function with `throws` keyword to not mess with the bulky `do { } catch { }`. Test fails automatically upon exception. -4. Start the inspection with `.inspect()` function +2. Annotate the test function with `throws` keyword to not mess with the bulky `do { } catch { }`. Test fails automatically upon exception. +3. Start the inspection with `.inspect()` function After the `.inspect()` call you need to repeat the structure of the `body` by chaining corresponding functions named after the SwiftUI views. @@ -173,7 +170,7 @@ let cell = try title.find(TableViewCell.self, relation: .parent) let cell = try sut.find(TableViewCell.self, containing: "Cell's title") ``` -This function accepts either `Inspectable` custom view or types like `ViewType.HStack`, searches for a specific `Text` first and then locates the parent view of a given type. +This function accepts either a custom view type or types like `ViewType.HStack`, searches for a specific `Text` first and then locates the parent view of a given type. #### Generic `find` function @@ -493,12 +490,6 @@ struct MyViewModifier: ViewModifier { } ``` -Just like with the custom views, in order to inspect a custom `ViewModifier` extend it to conform to `Inspectable` protocol in the tests scope. - -```swift -extension MyViewModifier: Inspectable { } -``` - The following test shows how you can extract the `modifier` and its `content` placeholder view using `modifier(_ type: T.Type)` and `viewModifierContent()` inspection calls respectively: ```swift @@ -545,7 +536,7 @@ func testViewModifier() { Approach #2: ```swift -struct MyViewModifier: ViewModifier, Inspectable { +struct MyViewModifier: ViewModifier { let inspection = Inspection() // 1. diff --git a/guide_gestures.md b/guide_gestures.md index 7fe60d42..ac6c1163 100644 --- a/guide_gestures.md +++ b/guide_gestures.md @@ -22,7 +22,7 @@ A test can inspect a gesture attached to a view using the `gesture(_:including:) modifier. For example, consider the following view: ```Swift -struct TestGestureView1: View & Inspectable { +struct TestGestureView1: View { @State var tapped = false var body: some View { @@ -51,7 +51,7 @@ A test can inspect a gesture attached to a view using the `highPriorityGesture(_ view modifier. For example, consider the following view: ```Swift -struct TestGestureView2: View & Inspectable { +struct TestGestureView2: View { @State var tapped = false var body: some View { @@ -80,7 +80,7 @@ A test can inspect a gesture attached using the `simultaneousGesture(_:including view modifier. For example, consider the following view: ```Swift -struct TestGestureView3: View & Inspectable { +struct TestGestureView3: View { @State var tapped = false var body: some View { @@ -111,7 +111,7 @@ A test can inspect the mask used when a gesture was attached to a view hierarchy consider the following view: ```Swift -struct TestGestureView9: View & Inspectable { +struct TestGestureView9: View { @State var tapped = false var body: some View { @@ -176,7 +176,7 @@ A test can invoke the updating callbacks added to a gesture. For example, consid view: ```Swift -struct TestGestureView5: View & Inspectable { +struct TestGestureView5: View { @GestureState var isDetectingLongPress = false internal let inspection = Inspection() @@ -245,7 +245,7 @@ A test can invoke the changed callbacks added to a gesture. For example, conside view: ```Swift -struct TestGestureView6: View & Inspectable { +struct TestGestureView6: View { @GestureState var isDetectingLongPress = false @State var totalNumberOfTaps = 0 @@ -318,7 +318,7 @@ A test can invoke the ended callbacks added to a gesture. For example, consider view: ```Swift -struct TestGestureView7: View & Inspectable { +struct TestGestureView7: View { @GestureState var isDetectingLongPress = false @State var totalNumberOfTaps = 0 @State var doneCounting = false @@ -393,7 +393,7 @@ consider the following view: ```Swift #if os(macOS) -struct TestGestureView8: View & Inspectable { +struct TestGestureView8: View { @State var tapped = false var body: some View { @@ -450,7 +450,7 @@ view: ```Swift @available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) -struct TestGestureView10: View & Inspectable { +struct TestGestureView10: View { @State var scale: CGFloat = 1.0 @State var angle = Angle(degrees: 0) @@ -543,7 +543,7 @@ into the view, the example is useful for demonstration purposes: ```Swift @available(iOS 13.0, macOS 11.0, tvOS 13.0, watchOS 6.0, *) -struct TestGestureView12: View & Inspectable { +struct TestGestureView12: View { internal let inspection = Inspection() internal let publisher = PassthroughSubject() diff --git a/guide_styles.md b/guide_styles.md index 292adc3e..6f2721e4 100644 --- a/guide_styles.md +++ b/guide_styles.md @@ -212,7 +212,7 @@ func testCustomProgressViewStyle() throws { ## **Custom Styles** A custom style is a type that implements standard interaction behavior and/or a custom -appearance for all views that apply the custom style in a view hiearchy. +appearance for all views that apply the custom style in a view hierarchy. A custom style starts with a protocol that concrete styles must conform to. Such a protocol has the following requirements: @@ -295,7 +295,7 @@ struct AnyHelloWorldStyle: HelloWorldStyle { To emulate SwiftUI's approach to styles, it is necessary to wrap setting the environment value. This not only encapsulates the type-erasure of the style, but it retains the type of the style as -part of the view's hiearchy. The following view modifier illustrates how to accomplish this: +part of the view's hierarchy. The following view modifier illustrates how to accomplish this: ```Swift struct HelloWorldStyleModifier: ViewModifier { @@ -319,7 +319,7 @@ extension View { ``` The following example illustrates how to define a concrete style and apply it to a view -hiearchy: +hierarchy: ```Swift struct Content: View { @@ -341,7 +341,7 @@ struct RedOutlineHelloWorldStyle: HelloWorldStyle { **ViewInspector** provides support for custom styles. -A test can verify the style applied to a view hiearchy. For example: +A test can verify the style applied to a view hierarchy. For example: ```Swift let sut = EmptyView().helloWorldStyle(RedOutlineHelloWorldStyle()) @@ -428,4 +428,4 @@ final class HelloWorldStyleTest: XCTestCase { ## Other topics - [Main guide](guide.md) -- [Gestures](guide_gestures.md) \ No newline at end of file +- [Gestures](guide_gestures.md) diff --git a/readiness.md b/readiness.md index 593a3a72..dfb04006 100644 --- a/readiness.md +++ b/readiness.md @@ -101,7 +101,7 @@ This document reflects the current status of the [ViewInspector](https://github. |:white_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:| Text | `string(locale: Locale) -> String`, `attributes: TextAttributes`, `attributedString: AttributedString`, `images: [Image]` | |:white_check_mark:| TextEditor | `input: String`, `setInput(_: String)` | |:white_check_mark:| TextField | `label view`, `callOnEditingChanged()`, `callOnCommit()`, `input: String`, `setInput(_: String)` | |:white_check_mark:| TimelineView | `contentView(Context)` |