SwiftUI Introspect lets you access the underlying UIKit or AppKit view for a SwiftUI view.
- How it works
- Install
- View Types
- Examples
- General Guidelines
- Advanced usage
- Note for library authors
- Community projects
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:
- Add marker views before and after
ScrollView
. - Traverse through all subviews between both marker views until a
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'
Button
ColorPicker
DatePicker
DatePicker
with.compact
styleDatePicker
with.field
styleDatePicker
with.graphical
styleDatePicker
with.stepperField
styleDatePicker
with.wheel
styleForm
Form
with.grouped
style.fullScreenCover
List
List
with.bordered
styleList
with.grouped
styleList
with.insetGrouped
styleList
with.inset
styleList
with.sidebar
styleListCell
Map
NavigationSplitView
NavigationStack
NavigationView
with.columns
styleNavigationView
with.stack
stylePageControl
Picker
with.menu
stylePicker
with.segmented
stylePicker
with.wheel
style.popover
ProgressView
with.circular
styleProgressView
with.linear
styleScrollView
.searchable
SecureField
.sheet
Slider
Stepper
Table
TabView
TabView
with.page
styleTextEditor
TextField
TextField
with.vertical
axisToggle
Toggle
withbutton
styleToggle
withcheckbox
styleToggle
withswitch
styleVideoPlayer
View
ViewController
WebView
Window
Missing 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:
- Use sparingly: prefer native SwiftUI modifiers when available. Use introspection only when you need underlying UIKit/AppKit APIs that SwiftUI does not expose.
- Program defensively: the introspection closure may be called multiple times during the view's lifecycle, such as during view updates or re-renders. Ensure that your customization code can handle being executed multiple times without causing unintended side effects.
- Avoid direct state changes: do not change SwiftUI state from inside the introspection closure. If you must update state, wrap it in
DispatchQueue.main.async
. - Test across OS versions: underlying implementations can differ by OS, which can affect customization.
- Avoid retain cycles: be cautious about capturing
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. - Scope:
.introspect
targets its receiver by default. Usescope: .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 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.