You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+75-25Lines changed: 75 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,14 +5,26 @@ SwiftUI Introspect
5
5
[](https://swiftpackageindex.com/siteline/swiftui-introspect)
SwiftUI Introspect allows you to get the underlying UIKit or AppKit element of a SwiftUI view.
9
-
10
-
For instance, with SwiftUI Introspect you can access `UITableView` to modify separators, or `UINavigationController` to customize the tab bar.
8
+
SwiftUI Introspect lets you access the underlying UIKit or AppKit view for a SwiftUI view.
9
+
10
+
-[How it works](#how-it-works)
11
+
-[Install](#install)
12
+
-[Swift Package Manager](#swift-package-manager)
13
+
-[CocoaPods](#cocoapods)
14
+
-[View Types](#view-types)
15
+
-[Examples](#examples)
16
+
-[General Guidelines](#general-guidelines)
17
+
-[Advanced usage](#advanced-usage)
18
+
-[Implement your own introspectable type](#implement-your-own-introspectable-type)
19
+
-[Introspect on future platform versions](#introspect-on-future-platform-versions)
20
+
-[Keep instances outside the customize closure](#keep-instances-outside-the-customize-closure)
21
+
-[Note for library authors](#note-for-library-authors)
22
+
-[Community projects](#community-projects)
11
23
12
24
How it works
13
25
------------
14
26
15
-
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.
27
+
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.
16
28
17
29
For instance, when introspecting a `ScrollView`...
18
30
@@ -27,13 +39,13 @@ ScrollView {
27
39
28
40
... it will:
29
41
30
-
1. Add marker views in front and behind`ScrollView`.
42
+
1. Add marker views before and after`ScrollView`.
31
43
2. Traverse through all subviews between both marker views until a `UIScrollView` instance (if any) is found.
32
44
33
45
> [!IMPORTANT]
34
-
> 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.
46
+
> Although this method is solid and unlikely to break on its own, future OS releases require explicit optin for introspection (`.iOS(.vXYZ)`) because underlying UIKit/AppKit types can change between major versions.
35
47
36
-
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`:
48
+
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`:
37
49
38
50
```swift
39
51
ScrollView {
@@ -46,7 +58,7 @@ ScrollView {
46
58
47
59
### Usage in production
48
60
49
-
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.
61
+
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.
**Missing an element?** Please [start a discussion](https://github.com/siteline/swiftui-introspect/discussions/new?category=ideas). As a temporary solution, you can [implement your own introspectable view type](#implement-your-own-introspectable-type).
Here are some guidelines to keep in mind when using SwiftUI Introspect:
225
+
226
+
-**Use sparingly**: prefer native SwiftUI modifiers when available. Use introspection only when you need underlying UIKit/AppKit APIs that SwiftUI does not expose.
227
+
-**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.
228
+
-**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`.
229
+
-**Test across OS versions**: underlying implementations can differ by OS, which can affect customization.
230
+
-**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.
231
+
-**Scope**: `.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.
232
+
198
233
Advanced usage
199
234
--------------
200
235
236
+
> [!NOTE]
237
+
> These features are advanced and unnecessary for most use cases. Use them when you need extra control or flexibility.
238
+
239
+
> [!IMPORTANT]
240
+
> To access these features, import SwiftUI Introspect using `@_spi(Advanced)` (see examples below).
241
+
201
242
### Implement your own introspectable type
202
243
203
244
**Missing an element?** Please [start a discussion](https://github.com/siteline/swiftui-introspect/discussions/new?category=ideas).
204
245
205
-
In case SwiftUI Introspect (unlikely) doesn't support the SwiftUI element that you're looking for, you can implement your own introspectable type.
246
+
In the unlikely event SwiftUI Introspect does not support the element you need, you can implement your own introspectable type.
206
247
207
248
For example, here's how the library implements the introspectable `TextField` type:
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.
261
-
262
-
For such cases, SwiftUI Introspect offers range-based platform version predicates behind the Advanced SPI:
301
+
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:
263
302
264
303
```swift
265
304
importSwiftUI
@@ -277,11 +316,11 @@ struct ContentView: View {
277
316
}
278
317
```
279
318
280
-
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.
319
+
Use this cautiously. Future OS versions may change underlying types, in which case the customization closure will not run unless support is explicitly declared.
281
320
282
321
### Keep instances outside the customize closure
283
322
284
-
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:
323
+
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:
285
324
286
325
```swift
287
326
importSwiftUI
@@ -301,10 +340,21 @@ struct ContentView: View {
301
340
}
302
341
```
303
342
343
+
Note for library authors
344
+
------------------------
345
+
346
+
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:
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](https://github.com/siteline/swiftui-introspect#introspect-on-future-platform-versions), it is already future proof without frequent version bumps.
353
+
304
354
Community projects
305
355
------------------
306
356
307
-
Here's a list of open source libraries powered by the SwiftUI Introspect library:
357
+
Here are some open source libraries powered by SwiftUI Introspect:
0 commit comments