SwiftUI Introspect

CI Status Badge Swift Version Compatibility Badge Platform Compatibility Badge

SwiftUI Introspect 允许您获取 SwiftUI 视图的底层 UIKit 或 AppKit 元素。

例如,使用 SwiftUI Introspect,您可以访问 UITableView 来修改分隔符,或 UINavigationController 来自定义标签栏。

工作原理

SwiftUI Introspect 的工作原理是在所选视图的顶部添加一个不可见的 IntrospectionView,并在其下方添加一个不可见的“锚点”视图,然后查看两者之间的 UIKit/AppKit 视图层级结构,以找到相关的视图。

例如,当内省一个 ScrollView 时...

ScrollView {
    Text("Item 1")
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { scrollView in
    // do something with UIScrollView
}

... 它将

  1. ScrollView 前后添加标记视图。
  2. 遍历两个标记视图之间的所有子视图,直到找到 UIScrollView 实例(如果有)。

重要提示

虽然这种内省方法非常可靠,并且本身不太可能被破坏,但未来的操作系统版本需要显式选择加入内省(.iOS(.vXYZ)),考虑到主要操作系统版本之间底层 UIKit/AppKit 视图类型的潜在差异。

默认情况下,.introspect 修饰符直接作用于其接收者。这意味着从您尝试内省的视图内部调用 .introspect 将不会有任何效果。但是,在某些情况下,这是不可能的,或者过于不灵活,在这种情况下,您可以内省一个祖先,但您必须通过覆盖内省 scope 来显式选择加入。

ScrollView {
    Text("Item 1")
        .introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18), scope: .ancestor) { scrollView in
            // do something with UIScrollView
        }
}

生产环境中的用法

SwiftUI Introspect 旨在用于生产环境。它不使用任何私有 API。它仅使用公开可用的方法检查视图层级结构。该库对检查视图层级结构采取防御性方法:没有硬性假设元素以某种方式布局,没有强制转换为 UIKit/AppKit 类,并且如果找不到 UIKit/AppKit 视图,则简单地忽略 .introspect 修饰符。

安装

Swift Package Manager

Xcode

Package.swift

let package = Package(
    dependencies: [
        .package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"),
    ],
    targets: [
        .target(name: <#Target Name#>, dependencies: [
            .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
        ]),
    ]
)

CocoaPods

pod 'SwiftUIIntrospect', '~> 1.0'

Introspection

已实现

缺少元素?发起讨论。作为临时解决方案,您可以实现您自己的可内省视图类型

无法实现

SwiftUI 受影响的框架 原因
Text UIKit, AppKit 不是 UILabel / NSLabel
Image UIKit, AppKit 不是 UIImageView / NSImageView
Button UIKit 不是 UIButton

示例

List

List {
    Text("Item")
}
.introspect(.list, on: .iOS(.v13, .v14, .v15)) { tableView in
    tableView.backgroundView = UIView()
    tableView.backgroundColor = .cyan
}
.introspect(.list, on: .iOS(.v16, .v17, .v18)) { collectionView in
    collectionView.backgroundView = UIView()
    collectionView.subviews.dropFirst(1).first?.backgroundColor = .cyan
}

ScrollView

ScrollView {
    Text("Item")
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { scrollView in
    scrollView.backgroundColor = .red
}

NavigationView

NavigationView {
    Text("Item")
}
.navigationViewStyle(.stack)
.introspect(.navigationView(style: .stack), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { navigationController in
    navigationController.navigationBar.backgroundColor = .cyan
}

TextField

TextField("Text Field", text: <#Binding<String>#>)
    .introspect(.textField, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { textField in
        textField.backgroundColor = .red
    }

高级用法

实现您自己的可内省类型

缺少元素?发起讨论

如果 SwiftUI Introspect(不太可能)不支持您正在寻找的 SwiftUI 元素,您可以实现您自己的可内省类型。

例如,以下是该库如何实现可内省的 TextField 类型

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

public struct TextFieldType: IntrospectableViewType {}

extension IntrospectableViewType where Self == TextFieldType {
    public static var textField: Self { .init() }
}

#if canImport(UIKit)
extension iOSViewVersion<TextFieldType, UITextField> {
    public static let v13 = Self(for: .v13)
    public static let v14 = Self(for: .v14)
    public static let v15 = Self(for: .v15)
    public static let v16 = Self(for: .v16)
    public static let v17 = Self(for: .v17)
    public static let v18 = Self(for: .v18)
}

extension tvOSViewVersion<TextFieldType, UITextField> {
    public static let v13 = Self(for: .v13)
    public static let v14 = Self(for: .v14)
    public static let v15 = Self(for: .v15)
    public static let v16 = Self(for: .v16)
    public static let v17 = Self(for: .v17)
    public static let v18 = Self(for: .v18)
}

extension visionOSViewVersion<TextFieldType, UITextField> {
    public static let v1 = Self(for: .v1)
    public static let v2 = Self(for: .v2)
}
#elseif canImport(AppKit)
extension macOSViewVersion<TextFieldType, NSTextField> {
    public static let v10_15 = Self(for: .v10_15)
    public static let v11 = Self(for: .v11)
    public static let v12 = Self(for: .v12)
    public static let v13 = Self(for: .v13)
    public static let v14 = Self(for: .v14)
    public static let v15 = Self(for: .v15)
}
#endif

在未来的平台版本上进行内省

默认情况下,内省应用于每个特定的平台版本。对于定期维护的代码库来说,这是一个合理的默认设置,以实现最大的可预测性,但对于例如库开发人员来说,这并不总是一个好的选择,他们可能希望涵盖尽可能多的未来平台版本,以便为他们的库提供长期未来功能的最佳机会,而无需定期维护。

对于这种情况,SwiftUI Introspect 在 Advanced SPI 背后提供了基于范围的平台版本谓词

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

struct ContentView: View {
    var body: some View {
        ScrollView {
            // ...
        }
        .introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
            // ...
        }
    }
}

请记住,应该谨慎使用此功能,并且充分了解,除非明确可用,否则任何未来的操作系统版本都可能破坏预期的内省类型。例如,如果在上面的示例中,假设 iOS 19 停止在底层使用 UIScrollView,则永远不会在所述平台上调用自定义闭包。

在自定义闭包之外保留实例

有时,您可能需要将内省实例保留比自定义闭包生命周期更长的时间。在这种情况下,@State 不是一个好的选择,因为它会产生 retain cycles。相反,SwiftUI Introspect 在 Advanced SPI 背后提供了 @Weak 属性包装器

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

struct ContentView: View {
    @Weak var scrollView: UIScrollView?

    var body: some View {
        ScrollView {
            // ...
        }
        .introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { scrollView in
            self.scrollView = scrollView
        }
    }
}

社区项目

以下是由 SwiftUI Introspect 库驱动的开源库列表

如果您正在开发基于 SwiftUI Introspect 构建的库,或者知道这样的库,请随时提交 PR 以将其添加到列表中。