Skip to content

Commit

Permalink
Finalize 0.9.3 release
Browse files Browse the repository at this point in the history
  • Loading branch information
nalexn authored Jan 14, 2023
2 parents 86d52f6 + 145a27f commit 724c7e3
Show file tree
Hide file tree
Showing 27 changed files with 870 additions and 230 deletions.
2 changes: 0 additions & 2 deletions .github/FUNDING.yml

This file was deleted.

4 changes: 2 additions & 2 deletions Sources/ViewInspector/BaseTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ extension SupplementaryChildrenLabelView {
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public protocol KnownViewType {
public protocol BaseViewType {
static var typePrefix: String { get }
static var namespacedPrefixes: [String] { get }
static var isTransitive: Bool { get }
static func inspectionCall(typeName: String) -> String
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public extension KnownViewType {
public extension BaseViewType {
static var namespacedPrefixes: [String] {
guard !typePrefix.isEmpty else { return [] }
return [.swiftUINamespaceRegex + typePrefix]
Expand Down
22 changes: 19 additions & 3 deletions Sources/ViewInspector/ContentExtraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ internal struct ContentExtractor {
}

internal func extractContent(environmentObjects: [AnyObject]) throws -> Any {
try validateSource()

try validateSourceBeforeExtraction()
switch contentSource {
case .view(let view):
return try view.extractContent(environmentObjects: environmentObjects)
Expand All @@ -29,6 +28,9 @@ internal struct ContentExtractor {
}
return .view(view)
case let viewModifier as any ViewModifier:
guard viewModifier.hasBody else {
throw InspectionError.notSupported("ViewModifier without the body")
}
return .viewModifier(viewModifier)
case let gesture as any Gesture:
return .gesture(gesture)
Expand All @@ -38,7 +40,7 @@ internal struct ContentExtractor {
}
}

private func validateSource() throws {
private func validateSourceBeforeExtraction() throws {
switch contentSource.source {
#if os(macOS)
case is any NSViewRepresentable:
Expand Down Expand Up @@ -98,6 +100,16 @@ internal struct ContentExtractor {
private let contentSource: ContentSource
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
private extension ViewModifier {
var hasBody: Bool {
if self is (any EnvironmentalModifier) {
return true
}
return Body.self != Never.self
}
}

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

Expand Down Expand Up @@ -126,6 +138,10 @@ public extension ViewModifier {
throw InspectionError
.missingEnvironmentObjects(view: view, objects: missingObjects)
}
if let envModifier = copy as? (any EnvironmentalModifier) {
let resolved = envModifier.resolve(in: EnvironmentValues())
return try resolved.extractContent(environmentObjects: environmentObjects)
}
return copy.body()
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/ViewInspector/InspectableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
import XCTest

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public struct InspectableView<View> where View: KnownViewType {
public struct InspectableView<View> where View: BaseViewType {

internal let content: Content
internal let parentView: UnwrappedView?
Expand Down Expand Up @@ -108,14 +108,14 @@ internal extension UnwrappedView {
return try .init(content, parent: parentView, call: inspectionCall, index: inspectionIndex)
}

func asInspectableView<T>(ofType type: T.Type) throws -> InspectableView<T> where T: KnownViewType {
func asInspectableView<T>(ofType type: T.Type) throws -> InspectableView<T> where T: BaseViewType {
return try .init(content, parent: parentView, call: inspectionCall, index: inspectionIndex)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
internal extension InspectableView {
func asInspectableView<T>(ofType type: T.Type) throws -> InspectableView<T> where T: KnownViewType {
func asInspectableView<T>(ofType type: T.Type) throws -> InspectableView<T> where T: BaseViewType {
return try .init(content, parent: parentView, call: inspectionCall, index: inspectionIndex)
}
}
Expand Down
19 changes: 17 additions & 2 deletions Sources/ViewInspector/Inspector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ internal extension Inspector {
return casted
}

static func unsafeMemoryRebind<V, T>(value: V, type: T.Type) throws -> T {
guard MemoryLayout<V>.size == MemoryLayout<T>.size else {
throw InspectionError.notSupported(
"""
Unable to rebind value of type \(Inspector.typeName(value: value, namespaced: true)) \
to \(Inspector.typeName(type: type, namespaced: true)). \
This is an internal library error, please open a ticket with these details.
""")
}
return withUnsafeBytes(of: value) { bytes in
return bytes.baseAddress!
.assumingMemoryBound(to: T.self).pointee
}
}

enum GenericParameters {
case keep
case remove
Expand All @@ -66,7 +81,7 @@ internal extension Inspector {

private static func isSystemType(name: String) -> Bool {
return [
String.swiftUINamespaceRegex,
String.swiftUINamespaceRegex, "Swift\\.",
"_CoreLocationUI_SwiftUI\\.", "_MapKit_SwiftUI\\.",
"_AuthenticationServices_SwiftUI\\.", "_AVKit_SwiftUI\\.",
].containsPrefixRegex(matching: name, wholeMatch: false)
Expand All @@ -91,7 +106,7 @@ internal extension Inspector {
private extension String {
func sanitizingNamespace() -> String {
var str = self
if let range = str.range(of: ".(unknown context at ") {
while let range = str.range(of: ".(unknown context at ") {
let end = str.index(range.upperBound, offsetBy: .init(11))
str.replaceSubrange(range.lowerBound..<end, with: "")
}
Expand Down
76 changes: 45 additions & 31 deletions Sources/ViewInspector/SwiftUI/NavigationLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ public extension ViewType {
extension ViewType.NavigationLink: SingleViewContent {

public static func child(_ content: Content) throws -> Content {
return try children(content).element(at: 0)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationLink: MultipleViewContent {

public static func children(_ content: Content) throws -> LazyGroup<Content> {
if let isActive = try? content.isActive(), !isActive {
throw InspectionError.viewNotFound(parent: "NavigationLink's destination")
}
let view = try Inspector.attribute(label: "destination", value: content.view)
let medium = content.medium.resettingViewModifiers()
return try Inspector.unwrap(view: view, medium: medium)
return try Inspector.viewsInContainer(view: view, medium: content.medium)
}
}

Expand Down Expand Up @@ -56,48 +66,52 @@ public extension InspectableView where View == ViewType.NavigationLink {
}

func isActive() throws -> Bool {
guard let external = try isActiveBinding() else {
return try isActiveState().wrappedValue
}
return external.wrappedValue
return try content.isActive()
}

func activate() throws { try set(isActive: true) }

func deactivate() throws { try set(isActive: false) }
func activate() throws {
try content.set(isActive: true)
}

private func set(isActive: Bool) throws {
if let external = try isActiveBinding() {
external.wrappedValue = isActive
} else {
// @State mutation from outside is ignored by SwiftUI
// try isActiveState().wrappedValue = isActive
// swiftlint:disable line_length
throw InspectionError.notSupported("Enable programmatic navigation by using `NavigationLink(destination:, tag:, selection:)`")
// swiftlint:enable line_length
}
func deactivate() throws {
try content.set(isActive: false)
}
}

// MARK: - Private

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
private extension InspectableView where View == ViewType.NavigationLink {
func isActiveState() throws -> State<Bool> {
if #available(iOS 14, tvOS 14, macOS 10.16, *) {
return try Inspector
.attribute(path: "_isActive|state", value: content.view, type: State<Bool>.self)
private extension Content {

func isActive() throws -> Bool {
if let binding = try? isActiveBinding() {
return binding.wrappedValue
}
return try Inspector
.attribute(label: "__internalIsActive", value: content.view, type: State<Bool>.self)
return try isActiveState().wrappedValue
}

func isActiveBinding() throws -> Binding<Bool>? {
func set(isActive: Bool) throws {
if let binding = try? isActiveBinding() {
binding.wrappedValue = isActive
return
}
try isActiveState().wrappedValue = isActive
}

private func isActiveState() throws -> State<Bool> {
throw InspectionError.notSupported(
"""
Please use `NavigationLink(destination:, tag:, selection:)` \
if you need to access the state value for reading or writing.
""")
}

private func isActiveBinding() throws -> Binding<Bool> {
if #available(iOS 14, tvOS 14, macOS 10.16, *) {
return try? Inspector
.attribute(path: "_isActive|binding", value: content.view, type: Binding<Bool>.self)
return try Inspector
.attribute(path: "_isActive|binding", value: view, type: Binding<Bool>.self)
}
return try? Inspector
.attribute(label: "_externalIsActive", value: content.view, type: Binding<Bool>.self)
return try Inspector
.attribute(label: "_externalIsActive", value: view, type: Binding<Bool>.self)
}
}
86 changes: 86 additions & 0 deletions Sources/ViewInspector/SwiftUI/NavigationSplitView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import SwiftUI

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

struct NavigationSplitView: KnownViewType {
public static var typePrefix: String = "NavigationSplitView"
}
}

// MARK: - Content Extraction

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationSplitView: SingleViewContent {

public static func child(_ content: Content) throws -> Content {
return try children(content).element(at: 0)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationSplitView: MultipleViewContent {

public static func children(_ content: Content) throws -> LazyGroup<Content> {
let view = try Inspector.attribute(label: "content", value: content.view)
return try Inspector.viewsInContainer(view: view, medium: content.medium)
}
}

// MARK: - Extraction from SingleViewContent parent

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View: SingleViewContent {

func navigationSplitView() throws -> InspectableView<ViewType.NavigationSplitView> {
return try .init(try child(), parent: self)
}
}

// MARK: - Extraction from MultipleViewContent parent

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View: MultipleViewContent {

func navigationSplitView(_ index: Int) throws -> InspectableView<ViewType.NavigationSplitView> {
return try .init(try child(at: index), parent: self, index: index)
}
}

// MARK: - Non Standard Children

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationSplitView: SupplementaryChildren {
static func supplementaryChildren(_ parent: UnwrappedView) throws -> LazyGroup<SupplementaryView> {
return .init(count: 2) { index in
let medium = parent.content.medium.resettingViewModifiers()
if index == 0 {
let child = try Inspector.attribute(label: "detail", value: parent.content.view)
let content = try Inspector.unwrap(content: Content(child, medium: medium))
return try InspectableView<ViewType.ClassifiedView>(
content, parent: parent, call: "detailView()")
} else {
let child = try Inspector.attribute(label: "sidebar", value: parent.content.view)
let content = try Inspector.unwrap(content: Content(child, medium: medium))
return try InspectableView<ViewType.ClassifiedView>(
content, parent: parent, call: "sidebarView()")
}
}
}
}

// MARK: - Custom Attributes

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View == ViewType.NavigationSplitView {

func detailView() throws -> InspectableView<ViewType.ClassifiedView> {
return try View.supplementaryChildren(self).element(at: 0)
.asInspectableView(ofType: ViewType.ClassifiedView.self)
}

func sidebarView() throws -> InspectableView<ViewType.ClassifiedView> {
return try View.supplementaryChildren(self).element(at: 1)
.asInspectableView(ofType: ViewType.ClassifiedView.self)
}
}
48 changes: 48 additions & 0 deletions Sources/ViewInspector/SwiftUI/NavigationStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SwiftUI

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

struct NavigationStack: KnownViewType {
public static var typePrefix: String = "NavigationStack"
}
}

// MARK: - Content Extraction

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationStack: SingleViewContent {

public static func child(_ content: Content) throws -> Content {
return try children(content).element(at: 0)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationStack: MultipleViewContent {

public static func children(_ content: Content) throws -> LazyGroup<Content> {
let view = try Inspector.attribute(label: "root", value: content.view)
return try Inspector.viewsInContainer(view: view, medium: content.medium)
}
}

// MARK: - Extraction from SingleViewContent parent

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View: SingleViewContent {

func navigationStack() throws -> InspectableView<ViewType.NavigationStack> {
return try .init(try child(), parent: self)
}
}

// MARK: - Extraction from MultipleViewContent parent

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View: MultipleViewContent {

func navigationStack(_ index: Int) throws -> InspectableView<ViewType.NavigationStack> {
return try .init(try child(at: index), parent: self, index: index)
}
}
8 changes: 8 additions & 0 deletions Sources/ViewInspector/SwiftUI/NavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ public extension ViewType {

// MARK: - Content Extraction

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationView: SingleViewContent {

public static func child(_ content: Content) throws -> Content {
return try children(content).element(at: 0)
}
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
extension ViewType.NavigationView: MultipleViewContent {

Expand Down
Loading

0 comments on commit 724c7e3

Please sign in to comment.