README.md
SwiftUI Introspect lets you access the underlying UIKit or AppKit view for a SwiftUI view.
SwiftUI Introspect adds an invisible IntrospectionView above the selected view and an invisible anchor below it, then searches the UIKit/AppKit view hierarchy between them to find the relevant view.
For instance, when introspecting a ScrollView...
ScrollView {
Text("Item 1")
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) { scrollView in
// do something with UIScrollView
}
... it will:
ScrollView.UIScrollView instance (if any) is found.[!IMPORTANT] Although this method is solid and unlikely to break on its own, future OS releases require explicit opt in for introspection (
.iOS(.vXYZ)) because underlying UIKit/AppKit types can change between major versions.
By default, .introspect acts on its receiver. Calling .introspect from inside the view you want to introspect has no effect. If you need to introspect an ancestor instead, set scope: .ancestor:
ScrollView {
Text("Item 1")
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26), scope: .ancestor) { scrollView in
// do something with UIScrollView
}
}
SwiftUI Introspect is suitable for production. It does not use private APIs. It inspects the view hierarchy using public methods and takes a defensive approach: it makes no hard layout assumptions, performs no forced casts to UIKit/AppKit classes, and ignores .introspect when the expected UIKit/AppKit view cannot be found.
let package = Package(
dependencies: [
.package(url: "https://github.com/siteline/swiftui-introspect", from: "26.0.0"),
],
targets: [
.target(name: <#Target Name#>, dependencies: [
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
]),
]
)
pod 'SwiftUIIntrospect', '~> 26.0.0'
ButtonColorPickerDatePickerDatePicker with .compact styleDatePicker with .field styleDatePicker with .graphical styleDatePicker with .stepperField styleDatePicker with .wheel styleFormForm with .grouped style.fullScreenCoverListList with .bordered styleList with .grouped styleList with .insetGrouped styleList with .inset styleList with .sidebar styleListCellMapNavigationSplitViewNavigationStackNavigationView with .columns styleNavigationView with .stack stylePageControlPicker with .menu stylePicker with .segmented stylePicker with .wheel style.popoverProgressView with .circular styleProgressView with .linear styleScrollView.searchableSecureField.sheetSliderStepperTableTabViewTabView with .page styleTextEditorTextFieldTextField with .vertical axisToggleToggle with button styleToggle with checkbox styleToggle with switch styleVideoPlayerViewViewControllerWebViewWindowMissing an element? Please start a discussion. As a temporary solution, you can implement your own introspectable view type.
| SwiftUI | Affected Frameworks | Why |
|---|---|---|
| Text | UIKit, AppKit | Not a UILabel / NSLabel |
| Image | UIKit, AppKit | Not a UIImageView / NSImageView |
| Button | UIKit | Not a UIButton |
| Link | UIKit, AppKit | Not a UIButton / NSButton |
| NavigationLink | UIKit | Not a UIButton |
| GroupBox | AppKit | No underlying view |
| Menu | UIKit, AppKit | No underlying view |
| Spacer | UIKit, AppKit | No underlying view |
| Divider | UIKit, AppKit | No underlying view |
| HStack, VStack, ZStack | UIKit, AppKit | No underlying view |
| LazyVStack, LazyHStack, LazyVGrid, LazyHGrid | UIKit, AppKit | No underlying view |
| Color | UIKit, AppKit | No underlying view |
| ForEach | UIKit, AppKit | No underlying view |
| GeometryReader | UIKit, AppKit | No underlying view |
| Chart | UIKit, AppKit | Native SwiftUI framework |
List {
Text("Item")
}
.introspect(.list, on: .iOS(.v13, .v14, .v15)) { tableView in
tableView.bounces = false
}
.introspect(.list, on: .iOS(.v16, .v17, .v18, .v26)) { collectionView in
collectionView.bounces = false
}
ScrollView {
Text("Item")
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) { scrollView in
scrollView.bounces = false
}
NavigationView {
Text("Item")
}
.navigationViewStyle(.stack)
.introspect(.navigationView(style: .stack), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) { navigationController in
navigationController.navigationBar.backgroundColor = .cyan
}
TextField("Text Field", text: <#Binding<String>#>)
.introspect(.textField, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) { textField in
textField.backgroundColor = .red
}
Here are some guidelines to keep in mind when using SwiftUI Introspect:
DispatchQueue.main.async.self or other strong references within the introspection closure, as this can lead to memory leaks. Use [weak self] or [unowned self] capture lists as appropriate..introspect targets its receiver by default. Use scope: .ancestor only when you need to introspect an ancestor. In general, you shouldn't worry about this as each view type has sensible, predictable default scopes.[!NOTE] These features are advanced and unnecessary for most use cases. Use them when you need extra control or flexibility.
[!IMPORTANT] To access these features, import SwiftUI Introspect using
@_spi(Advanced)(see examples below).
Missing an element? Please start a discussion.
In the unlikely event SwiftUI Introspect does not support the element you need, you can implement your own introspectable type.
For example, here's how the library implements the introspectable TextField type:
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)
public static let v26 = Self(for: .v26)
}
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)
public static let v26 = Self(for: .v26)
}
extension visionOSViewVersion<TextFieldType, UITextField> {
public static let v1 = Self(for: .v1)
public static let v2 = Self(for: .v2)
public static let v26 = Self(for: .v26)
}
#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)
public static let v26 = Self(for: .v26)
}
#endif
By default, introspection targets specific platform versions. This is an intentional design decision to maintain maximum predictability in actively maintained apps. However library authors may prefer to cover future versions to limit their commitment to regular maintenance without breaking client apps. For that, SwiftUI Introspect provides range-based version predicates via the Advanced SPI:
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
struct ContentView: View {
var body: some View {
ScrollView {
// ...
}
.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
// ...
}
}
}
Use this cautiously. Future OS versions may change underlying types, in which case the customization closure will not run unless support is explicitly declared.
Sometimes you need to keep an introspected instance beyond the customization closure. @State is not appropriate for this, as it can create retain cycles. Instead, SwiftUI Introspect offers a @Weak property wrapper behind the Advanced SPI:
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, .v26)) { scrollView in
self.scrollView = scrollView
}
}
}
If your library depends on SwiftUI Introspect, declare a version range that spans at least the last two major versions instead of jumping straight to the latest. This avoids conflicts when apps pull the library directly and through multiple dependencies. For example:
.package(url: "https://github.com/siteline/swiftui-introspect", "1.3.0"..<"27.0.0"),
A wider range is safe because SwiftUI Introspect is essentially “finished”: no new features will be added, only newer platform versions and view types. Thanks to @_spi(Advanced) imports, it is already future proof without frequent version bumps.
Here are some popular open source libraries powered by SwiftUI Introspect:
If you're working on a library built on SwiftUI Introspect or know of one, feel free to submit a PR adding it to the list.