SwiftUI Introspect allows you to get the underlying UIKit or AppKit element of a SwiftUI view.
For instance, with SwiftUI Introspect you can access UITableView
to modify separators, or UINavigationController
to customize the tab bar.
SwiftUI Introspect works by adding an invisible IntrospectionView
on top of the selected view, and an invisible "anchor" view underneath it, then looking through the UIKit/AppKit view hierarchy between the two 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)) { scrollView in
// do something with UIScrollView
}
... it will:
- Add marker views in front and behind
ScrollView
. - Traverse through all subviews between both marker views until a
UIScrollView
instance (if any) is found.
Important
Although this introspection method is very solid and unlikely to break in itself, future OS releases require explicit opt-in for introspection (.iOS(.vXYZ)
), given potential differences in underlying UIKit/AppKit view types between major OS versions.
By default, the .introspect
modifier acts directly on its receiver. This means calling .introspect
from inside the view you're trying to introspect won't have any effect. However, there are times when this is not possible or simply too inflexible, in which case you can introspect an ancestor, but you must opt into this explicitly by overriding the introspection 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 is meant to be used in production. It does not use any private API. It only inspects the view hierarchy using publicly available methods. The library takes a defensive approach to inspecting the view hierarchy: there is no hard assumption that elements are laid out a certain way, there is no force-cast to UIKit/AppKit classes, and the .introspect
modifier is simply ignored if UIKit/AppKit views cannot be found.
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
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
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 |
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
}
Missing an element? Please start a discussion.
In case SwiftUI Introspect (unlikely) doesn't support the SwiftUI element that you're looking for, 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)
}
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
By default, introspection applies per specific platform version. This is a sensible default for maximum predictability in regularly maintained codebases, but it's not always a good fit for e.g. library developers who may want to cover as many future platform versions as possible in order to provide the best chance for long-term future functionality of their library without regular maintenance.
For such cases, SwiftUI Introspect offers range-based platform version predicates behind the Advanced SPI:
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
struct ContentView: View {
var body: some View {
ScrollView {
// ...
}
.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
// ...
}
}
}
Bear in mind this should be used cautiously, and with full knowledge that any future OS version might break the expected introspection types unless explicitly available. For instance, if in the example above hypothetically iOS 19 stops using UIScrollView under the hood, the customization closure will never be called on said platform.
Sometimes, you might need to keep your introspected instance around for longer than the customization closure lifetime. In such cases, @State
is not a good option because it produces 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)) { scrollView in
self.scrollView = scrollView
}
}
}
Here's a list of open source libraries powered by the SwiftUI Introspect library:
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.