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
}
... 它将
ScrollView
前后添加标记视图。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
修饰符。
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"),
]),
]
)
pod 'SwiftUIIntrospect', '~> 1.0'
Button
ColorPicker
DatePicker
DatePicker
,样式为 .compact
DatePicker
,样式为 .field
DatePicker
,样式为 .graphical
DatePicker
,样式为 .stepperField
DatePicker
,样式为 .wheel
Form
Form
,样式为 .grouped
.fullScreenCover
List
List
,样式为 .bordered
List
,样式为 .grouped
List
,样式为 .insetGrouped
List
,样式为 .inset
List
,样式为 .sidebar
ListCell
Map
NavigationSplitView
NavigationStack
NavigationView
,样式为 .columns
NavigationView
,样式为 .stack
PageControl
Picker
,样式为 .menu
Picker
,样式为 .segmented
Picker
,样式为 .wheel
.popover
ProgressView
,样式为 .circular
ProgressView
,样式为 .linear
ScrollView
.searchable
SecureField
.sheet
Slider
Stepper
Table
TabView
TabView
,样式为 .page
TextEditor
TextField
TextField
,轴向为 .vertical
Toggle
Toggle
,样式为 button
Toggle
,样式为 checkbox
Toggle
,样式为 switch
VideoPlayer
View
ViewController
Window
缺少元素? 请发起讨论。作为临时解决方案,您可以实现您自己的可内省视图类型。
SwiftUI | 受影响的框架 | 原因 |
---|---|---|
Text | UIKit, AppKit | 不是 UILabel / NSLabel |
Image | UIKit, AppKit | 不是 UIImageView / NSImageView |
Button | UIKit | 不是 UIButton |
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 {
Text("Item")
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { scrollView in
scrollView.backgroundColor = .red
}
NavigationView {
Text("Item")
}
.navigationViewStyle(.stack)
.introspect(.navigationView(style: .stack), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { navigationController in
navigationController.navigationBar.backgroundColor = .cyan
}
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 以将其添加到列表中。