Skip to content

Commit

Permalink
Merge branch '0.9.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexey Naumov committed Dec 30, 2021
2 parents 0d54687 + 5bb5df3 commit 6b88c4e
Show file tree
Hide file tree
Showing 104 changed files with 2,894 additions and 929 deletions.
72 changes: 60 additions & 12 deletions .watchOS/watchOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions Sources/ViewInspector/BaseTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,23 @@ extension InspectionError: CustomStringConvertible, LocalizedError {
}
}

// MARK: - ViewProvider

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
internal protocol SingleViewProvider {
func view() throws -> Any
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
internal protocol MultipleViewProvider {
func views() throws -> LazyGroup<Any>
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
internal protocol ElementViewProvider {
func view(_ element: Any) throws -> Any
}

// MARK: - BinaryEquatable

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
Expand Down
47 changes: 39 additions & 8 deletions Sources/ViewInspector/InspectableView+RandomAccessCollection.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import SwiftUI

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension InspectableView: Sequence where View: MultipleViewContent {
Expand All @@ -7,17 +8,19 @@ extension InspectableView: Sequence where View: MultipleViewContent {

public struct Iterator: IteratorProtocol {

private var groupIterator: LazyGroup<Content>.Iterator
private var index: Int = -1
private let group: LazyGroup<Content>
private let view: UnwrappedView

init(_ group: LazyGroup<Content>, view: UnwrappedView) {
groupIterator = group.makeIterator()
self.group = group
self.view = view
}

mutating public func next() -> Element? {
guard let content = groupIterator.next()
else { return nil }
index += 1
guard index < group.count else { return nil }
let content = (try? group.element(at: index)) ?? .absentView
return try? .init(content, parent: view)
}
}
Expand Down Expand Up @@ -45,11 +48,39 @@ extension InspectableView: Collection, BidirectionalCollection, RandomAccessColl
public var count: Int { underestimatedCount }

public subscript(index: Index) -> Iterator.Element {
// swiftlint:disable force_try
let viewes = try! View.children(content)
return try! .init(try! viewes.element(at: index), parent: self, call: "[\(index)]")
// swiftlint:enable force_try
do {
do {
let viewes = try View.children(content)
return try .init(try viewes.element(at: index), parent: self, call: "[\(index)]")
} catch InspectionError.viewNotFound {
return try Element(.absentView, parent: self, index: index)
} catch { throw error }
} catch {
fatalError(error.localizedDescription)
}
}

public func index(after index: Index) -> Index { index + 1 }
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension Content {

static var absentView: Content {
return Content(AbsentView())
}

var isAbsent: Bool { view is AbsentView }
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
internal struct AbsentView: View {
var body: Never { fatalError() }
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension InspectableView {
public var isAbsent: Bool {
return content.isAbsent
}
}
40 changes: 28 additions & 12 deletions Sources/ViewInspector/InspectableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ public struct InspectableView<View> where View: KnownViewType {
inspectionCall: inspectionCall)
} catch {
if let err = error as? InspectionError, case .typeMismatch = err {
let factual = Inspector.typeName(value: content.view, namespaced: true, prefixOnly: true)
.sanitizeNamespace()
let factual = Inspector.typeName(value: content.view, namespaced: true)
.removingSwiftUINamespace()
let expected = View.namespacedPrefixes
.map { $0.sanitizeNamespace() }
.map { $0.removingSwiftUINamespace() }
.joined(separator: " or ")
throw InspectionError.inspection(path: pathToRoot, factual: factual, expected: expected)
}
Expand All @@ -49,13 +49,9 @@ public struct InspectableView<View> where View: KnownViewType {
}

private extension String {
func sanitizeNamespace() -> String {
var str = self
if let range = str.range(of: ".(unknown context at ") {
let end = str.index(range.upperBound, offsetBy: .init(11))
str.replaceSubrange(range.lowerBound..<end, with: "")
}
return str.replacingOccurrences(of: "SwiftUI.", with: "")
func removingSwiftUINamespace() -> String {
guard hasPrefix("SwiftUI.") else { return self }
return String(suffix(count - 8))
}
}

Expand Down Expand Up @@ -95,6 +91,11 @@ internal extension InspectableView {

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public extension InspectableView {
/**
A function for accessing the parent view for introspection
- Returns: immediate predecessor of the current view in the hierarchy
- Throws: if the current view is the root
*/
func parent() throws -> InspectableView<ViewType.ParentView> {
guard let parent = self.parentView else {
throw InspectionError.parentViewNotFound(view: Inspector.typeName(value: content.view))
Expand All @@ -108,10 +109,22 @@ public extension InspectableView {
return try .init(parent.content, parent: parent.parentView, call: parent.inspectionCall)
}

/**
A property for obtaining the inspection call's chain from the root view to the current view
- Returns: A `String` representation of the inspection calls' chain
*/
var pathToRoot: String {
let prefix = parentView.flatMap { $0.pathToRoot } ?? ""
return prefix.isEmpty ? inspectionCall : prefix + "." + inspectionCall
}

/**
A function for erasing the type of the current view
- Returns: The current view represented as a `ClassifiedView`
*/
func classified() throws -> InspectableView<ViewType.ClassifiedView> {
return try asInspectableView()
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
Expand Down Expand Up @@ -309,7 +322,7 @@ extension ModifierNameProvider {
extension ModifiedContent: ModifierNameProvider {

func modifierType(prefixOnly: Bool) -> String {
return Inspector.typeName(type: Modifier.self, prefixOnly: prefixOnly)
return Inspector.typeName(type: Modifier.self, generics: prefixOnly ? .remove : .keep)
}

var customModifier: Inspectable? {
Expand Down Expand Up @@ -339,7 +352,7 @@ public extension InspectableView {
}

internal func guardIsResponsive() throws {
let name = Inspector.typeName(value: content.view, prefixOnly: true)
let name = Inspector.typeName(value: content.view, generics: .remove)
if isDisabled() {
let blocker = farthestParent(where: { $0.isDisabled() }) ?? self
throw InspectionError.unresponsiveControl(
Expand All @@ -355,6 +368,9 @@ public extension InspectableView {
throw InspectionError.unresponsiveControl(
name: name, reason: blocker.pathToRoot.ifEmpty(use: "it") + " has allowsHitTesting set to false")
}
if isAbsent {
throw InspectionError.unresponsiveControl(name: name, reason: "the conditional view has been hidden")
}
}

private func farthestParent(where condition: (InspectableView<ViewType.ClassifiedView>) -> Bool) -> UnwrappedView? {
Expand Down
114 changes: 76 additions & 38 deletions Sources/ViewInspector/Inspector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,62 @@ internal extension Inspector {
return casted
}

enum GenericParameters {
case keep
case remove
case customViewPlaceholder
}

static func typeName(value: Any,
namespaced: Bool = false,
prefixOnly: Bool = false) -> String {
return typeName(type: type(of: value), namespaced: namespaced, prefixOnly: prefixOnly)
generics: GenericParameters = .keep) -> String {
return typeName(type: type(of: value), namespaced: namespaced,
generics: generics)
}

static func typeName(type: Any.Type,
namespaced: Bool = false,
prefixOnly: Bool = false) -> String {
let typeName = namespaced ? String(reflecting: type) : String(describing: type)
guard prefixOnly else { return typeName }
let name = typeName.components(separatedBy: "<").first!
guard namespaced else { return name }
let string = NSMutableString(string: name)
let range = NSRange(location: 0, length: string.length)
namespaceSanitizeRegex.replaceMatches(in: string, options: [], range: range, withTemplate: "SwiftUI")
return String(string)
}

private static var namespaceSanitizeRegex: NSRegularExpression = {
guard let regex = try? NSRegularExpression(pattern: "SwiftUI.\\(unknown context at .*\\)", options: [])
else { fatalError() }
return regex
}()
generics: GenericParameters = .keep) -> String {
let typeName = namespaced ? String(reflecting: type).sanitizingNamespace() : String(describing: type)
switch generics {
case .keep:
return typeName
case .remove:
return typeName.replacingGenericParameters("")
case .customViewPlaceholder:
let parameters = ViewType.customViewGenericsPlaceholder
return typeName.replacingGenericParameters(parameters)
}
}
}

private extension String {
func sanitizingNamespace() -> String {
var str = self
if let range = str.range(of: ".(unknown context at ") {
let end = str.index(range.upperBound, offsetBy: .init(11))
str.replaceSubrange(range.lowerBound..<end, with: "")
}
return str
}

func replacingGenericParameters(_ replacement: String) -> String {
guard let start = self.firstIndex(of: "<")
else { return self }
var balance = 1
var current = self.index(after: start)
while balance > 0 && current < endIndex {
let char = self[current]
if char == "<" { balance += 1 }
if char == ">" { balance -= 1 }
current = self.index(after: current)
}
if balance == 0 {
return String(self[..<start]) + replacement +
String(self[current...]).replacingGenericParameters(replacement)
}
return self
}
}

// MARK: - Attributes lookup
Expand Down Expand Up @@ -180,7 +212,7 @@ internal extension Inspector {
}

static func isTupleView(_ view: Any) -> Bool {
return Inspector.typeName(value: view, prefixOnly: true) == ViewType.TupleView.typePrefix
return Inspector.typeName(value: view, generics: .remove) == ViewType.TupleView.typePrefix
}

static func unwrap(view: Any, medium: Content.Medium) throws -> Content {
Expand All @@ -189,7 +221,7 @@ internal extension Inspector {

// swiftlint:disable cyclomatic_complexity
static func unwrap(content: Content) throws -> Content {
switch Inspector.typeName(value: content.view, prefixOnly: true) {
switch Inspector.typeName(value: content.view, generics: .remove) {
case "Tree":
return try ViewType.TreeView.child(content)
case "IDView":
Expand All @@ -210,6 +242,8 @@ internal extension Inspector {
return try ViewType.EnvironmentReaderView.child(content)
case "_DelayedPreferenceView":
return try ViewType.DelayedPreferenceView.child(content)
case "_PreferenceReadingView":
return try ViewType.PreferenceReadingView.child(content)
default:
return content
}
Expand All @@ -218,23 +252,23 @@ internal extension Inspector {

static func guardType(value: Any, namespacedPrefixes: [String], inspectionCall: String) throws {

for prefix in namespacedPrefixes {
let withGenericParams = prefix.contains("<")
let typePrefix = typeName(type: type(of: value), namespaced: true, prefixOnly: !withGenericParams)
if typePrefix == "SwiftUI.EnvironmentReaderView" {
let typeWithParams = typeName(type: type(of: value))
if typeWithParams.contains("NavigationBarItemsKey") {
throw InspectionError.notSupported(
"""
Please insert '.navigationBarItems()' before \(inspectionCall) \
for unwrapping the underlying view hierarchy.
""")
}
}
if namespacedPrefixes.contains(typePrefix) {
return
var typePrefix = typeName(type: type(of: value), namespaced: true, generics: .remove)
if typePrefix == ViewType.popupContainerTypePrefix {
typePrefix = typeName(type: type(of: value), namespaced: true)
}
if typePrefix == "SwiftUI.EnvironmentReaderView" {
let typeWithParams = typeName(type: type(of: value))
if typeWithParams.contains("NavigationBarItemsKey") {
throw InspectionError.notSupported(
"""
Please insert '.navigationBarItems()' before \(inspectionCall) \
for unwrapping the underlying view hierarchy.
""")
}
}
if namespacedPrefixes.contains(typePrefix) {
return
}
if let prefix = namespacedPrefixes.first {
let typePrefix = typeName(type: type(of: value), namespaced: true)
throw InspectionError.typeMismatch(factual: typePrefix, expected: prefix)
Expand All @@ -245,8 +279,12 @@ internal extension Inspector {
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension InspectionError {
static func typeMismatch<V, T>(_ value: V, _ expectedType: T.Type) -> InspectionError {
return .typeMismatch(
factual: Inspector.typeName(value: value),
expected: Inspector.typeName(type: expectedType))
var factual = Inspector.typeName(value: value)
var expected = Inspector.typeName(type: expectedType)
if factual == expected {
factual = Inspector.typeName(value: value, namespaced: true)
expected = Inspector.typeName(type: expectedType, namespaced: true)
}
return .typeMismatch(factual: factual, expected: expected)
}
}
Loading

0 comments on commit 6b88c4e

Please sign in to comment.