Skip to content

Commit

Permalink
Merge pull request #218 from nalexn/0.9.3
Browse files Browse the repository at this point in the history
Finalize 0.9.3 release
  • Loading branch information
nalexn authored Dec 25, 2022
2 parents ac7df67 + fe0a15f commit 6b77fcf
Show file tree
Hide file tree
Showing 67 changed files with 653 additions and 494 deletions.
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ identifier_name:
min_length: 2

file_length:
warning: 500
warning: 600
error: 1000

function_parameter_count:
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
81 changes: 32 additions & 49 deletions Sources/ViewInspector/BaseTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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, *)
Expand Down
136 changes: 136 additions & 0 deletions Sources/ViewInspector/ContentExtraction.swift
Original file line number Diff line number Diff line change
@@ -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 { () }
}
27 changes: 14 additions & 13 deletions Sources/ViewInspector/EnvironmentInjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -18,24 +19,24 @@ internal extension Inspectable {
return "\(ivarName[1..<ivarName.count]): \(objName)"
}
}
mutating func inject(environmentObject: AnyObject) {
static func inject<T>(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<Self>.size
var offset = MemoryLayout<Self>.stride - envObjSize
let step = MemoryLayout<Self>.alignment
let viewSize = MemoryLayout<T>.size
var offset = MemoryLayout<T>.stride - envObjSize
let step = MemoryLayout<T>.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: &copy) { bytes in
guard bytes[offset..<offset + envObjSize].elementsEqual(reference)
else { return }
Expand All @@ -50,11 +51,11 @@ internal extension Inspectable {
let pointerToValue = rawPointer.assumingMemoryBound(to: EnvObject.Forgery.self)
pointerToValue.pointee = .init(object: environmentObject)
}
self = copy
return
return copy
}
offset -= step
}
return entity
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ extension InspectableView: Collection, BidirectionalCollection, RandomAccessColl
public subscript(index: Index) -> 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 }
Expand Down
Loading

0 comments on commit 6b77fcf

Please sign in to comment.