Library / SDK
siteline/swiftui-introspect avatar
siteline/swiftui-introspect

SwiftUI Introspect: reaching the UIKit and AppKit view behind a SwiftUI view

Introspect underlying UIKit/AppKit components from SwiftUI

6,560 stars418 forksSwiftMIT

At a glance

What is it?
SwiftUI Introspect adds marker views around a SwiftUI view and walks the native view hierarchy between them to hand you the underlying UIKit or AppKit object. It is for engineers who need a delegate, a scroll offset or a control property that SwiftUI does not expose.
Who is it for?
Adopt SwiftUI Introspect when you need a native delegate or a property SwiftUI does not surface, and you accept that each new major OS version needs an explicit `.iOS(.vXYZ)` opt in. Do not adopt it as a general styling layer, and do not expect it to introspect a view type that is not in the implemented list.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap SwiftUI Introspect fills for UIKit and AppKit interop

SwiftUI deliberately hides the native view objects it builds on. Most of the time that is the point. The trouble starts when you need something the framework does not expose: a `UIScrollView` delegate, a `UINavigationBar` appearance tweak, a text field's underlying control, or a `UIPageControl` instance. There is no supported SwiftUI modifier for those, and dropping back to a full `UIViewRepresentable` rewrite is heavy when you only need one object.

SwiftUI Introspect targets exactly that seam. The README frames the library as a way to "access the underlying UIKit or AppKit view for a SwiftUI view", and the implemented list is concrete: `Button`, `ColorPicker`, `DatePicker` (including the `.compact`, `.field`, `.graphical`, `.stepperField` and `.wheel` styles), `Form` and its `.grouped` style, `.fullScreenCover`, `List` with `.bordered`, `.grouped`, `.insetGrouped`, `.inset` and `.sidebar` styles, `ListCell`, `Map`, `NavigationSplitView`, `NavigationStack`, `NavigationView` with `.columns` and `.stack` styles, and `PageControl`. It is for app engineers already committed to SwiftUI who hit one of those specific walls, not for people building a UIKit app with a SwiftUI island.

How the marker-view traversal finds a UIScrollView

The mechanism is unusual and worth understanding before you depend on it. SwiftUI Introspect does not reflect on SwiftUI internals and it does not use private APIs. Instead it inserts an invisible `IntrospectionView` above the selected view and an invisible anchor below it, then searches the native view hierarchy between those two markers.

The README walks through the `ScrollView` case. When you call `.introspect(.scrollView, ...)`, the library adds marker views before and after the `ScrollView`, then traverses all subviews between both markers until it finds a `UIScrollView` instance. Two consequences follow from that design. First, the search is positional: if the expected native view is not present in that span, the library ignores the call rather than crashing. Second, the closure you write runs when the object is found, so you are mutating a live native object from inside SwiftUI's update cycle.

The README is explicit that the approach is defensive: no hard layout assumptions, no forced casts to UIKit or AppKit classes, and the call is skipped when the expected view cannot be found. That is a better failure mode than a crash, but it also means a silent no-op is possible. If your customize closure never runs, the library is telling you nothing about why.

One scoping rule trips people up. By default `.introspect` acts on its receiver, and calling it from inside the view you want to introspect has no effect. To reach an ancestor you pass `scope: .ancestor`.

Installing SwiftUI Introspect and introspecting a ScrollView

The README gives the Swift Package Manager route. Add the package to your dependencies with a version requirement, then add the product to your target. The current release line is 27.0.0, published on 2026-09-14, with 26.1.0 published the same day.

swift
.package(url: "https://github.com/siteline/swiftui-introspect", from: "27.0.0"),

Then add the dependency to your target:

swift
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),

With the package resolved, the first real use is the example the README itself gives. It attaches `.introspect` to a `ScrollView` and opts in to specific iOS versions:

swift
ScrollView {
	Text("Item 1")
}
.introspect(.scrollView, on: .iOS(.v17, .v18, .v26, .v27)) { scrollView in
	// do something with UIScrollView
}

What you should see is your closure running with a `UIScrollView` instance, at which point you can set a delegate or read content offset. The version list in `on:` is not decoration. The README states that future OS releases require explicit opt in because underlying UIKit and AppKit types can change between major versions, so the list is your compatibility contract. If you run on an iOS version you did not list, you get no introspection. Note also that the code above introspects the `ScrollView` it is attached to; if you attach it inside the `ScrollView` instead, the README says it has no effect, and you would need `scope: .ancestor`.

The version opt-in list is the real maintenance cost

The most important limitation is not a bug, it is the design. Every new major OS release requires you to add a version to the `on:` parameter. Miss it and introspection silently stops working on that OS. The library will not warn you, because the README's defensive approach is to ignore `.introspect` when the expected native view cannot be found.

That interacts badly with the way most teams test. A build that passes on the simulator you happen to run will pass whether or not the closure fired. The failure is a feature that quietly does nothing on a new OS, which is harder to notice than a crash. The README's own guidance is to opt in explicitly, and the practical reading is that you should treat the `on:` list as something you audit at each OS release, not something you set once.

The second constraint is coverage. The implemented list is long but finite. If your view type is not on it, `.introspect` is not going to find anything for you. The README does document an advanced path for implementing your own introspectable type, and it points to a `Sources/` directory in the repository layout, but the README does not spell out the full protocol contract. Expect to read the source if you need a type the library does not ship.

The README also notes that instances should be kept outside the customize closure. That is a hint about lifetime: the closure can run more than once, and state you create inside it does not survive the way you might assume.

SwiftUI Introspect compared with UIViewRepresentable and pure SwiftUI

The obvious alternative is `UIViewRepresentable`. It is the supported Apple path: you build the native view yourself, so you own the object from the start and no hierarchy search is involved. The difference in approach is total. `UIViewRepresentable` means the native view is the thing you wrote, and SwiftUI wraps it. SwiftUI Introspect means the native view is the thing SwiftUI wrote, and you go looking for it after the fact.

That makes `UIViewRepresentable` the right choice when you are replacing a control, and it makes it the wrong choice when you want to keep SwiftUI's own `List`, `Form` or `NavigationStack` behavior and only adjust one property. Rewriting a `List` as a representable means reimplementing selection, styling and platform behavior that SwiftUI already handles. Introspection is cheaper when the surface you need to touch is small.

The other alternative is to not do this at all: accept what SwiftUI exposes, or file the gap and wait. That is a legitimate answer for appearance tweaks, and a poor one for cases where a delegate is the only route to correct scrolling or gesture behavior. The README also points to a Community projects section, which suggests the maintainers see adjacent tools rather than a single canonical replacement, but the README does not name them.

A third option, private API or runtime reflection on SwiftUI internals, is what this library exists to avoid. The README states plainly that it does not use private APIs, which is the reason to prefer it over a homegrown hierarchy hack.

Licence, releases and what upgrading 26.1.0 to 27.0.0 involves

The repository is MIT licensed, which is permissive and places few obligations on how you redistribute the code. That is a statement about the licence text, not legal advice; if your organisation has a policy on third-party dependencies, run it through that.

On maintenance, the last push was on 2026-09-15, and releases 27.0.0 and 26.1.0 both landed on 2026-09-14, preceded by 27.0.0-rc.1 the same day. The repository is not archived. The release cadence here is tied to OS versions rather than to a calendar, so a gap in releases does not by itself mean the project is idle.

The upgrade cost is dominated by the `on:` version lists scattered through your codebase, not by API churn in the library. A major version bump like 26.1.0 to 27.0.0 is the moment to grep for `.introspect(` and confirm each call site lists every OS version you ship to. The README does not document a deprecation or migration path between major versions, so treat the release notes as the source for that. There is also an `Examples/` directory and a `Showcase` example package in the repository layout if you want a working reference before touching production code.

Editorial conclusion

Adopt SwiftUI Introspect when you need a native delegate or a property SwiftUI does not surface, and you accept that each new major OS version needs an explicit `.iOS(.vXYZ)` opt in. Do not adopt it as a general styling layer, and do not expect it to introspect a view type that is not in the implemented list. Before shipping, verify on the oldest and newest OS versions you support that the customize closure actually fires and that the object you receive is the one you expect.

Frequently asked questions

What are the downsides of using SwiftUI?

The library's own README points at one: SwiftUI hides the underlying UIKit and AppKit views, so properties and delegates that live on those objects are not reachable through the framework. SwiftUI Introspect exists to close that gap by searching the native hierarchy for the view you need.

Which is better in 2026, SwiftUI or UIKit?

The README does not compare the two frameworks and takes no position on which to choose. SwiftUI Introspect assumes you are already writing SwiftUI and only need occasional access to the native view underneath.

Is SwiftUI different from Swift?

The README does not discuss the distinction between the language and the UI framework. SwiftUI Introspect is a Swift package that bridges SwiftUI views to their UIKit and AppKit counterparts.

What does SwiftUI stand for?

The README does not define the name. SwiftUI Introspect treats SwiftUI as the declarative framework whose views it introspects, adding invisible markers around a view and searching the native hierarchy between them.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. siteline/swiftui-introspect on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/siteline-swiftui-introspect.svg)](https://hysenlabs.com/projects/siteline-swiftui-introspect)