Skip to content

Commit a08b87f

Browse files
authored
Update README for 26.0 release [skip ci] (#481)
1 parent ebec259 commit a08b87f

File tree

1 file changed

+75
-25
lines changed

1 file changed

+75
-25
lines changed

README.md

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@ SwiftUI Introspect
55
[![Swift Version Compatibility Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsiteline%2Fswiftui-introspect%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/siteline/swiftui-introspect)
66
[![Platform Compatibility Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsiteline%2Fswiftui-introspect%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/siteline/swiftui-introspect)
77

8-
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)
1123

1224
How it works
1325
------------
1426

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.
1628

1729
For instance, when introspecting a `ScrollView`...
1830

@@ -27,13 +39,13 @@ ScrollView {
2739

2840
... it will:
2941

30-
1. Add marker views in front and behind `ScrollView`.
42+
1. Add marker views before and after `ScrollView`.
3143
2. Traverse through all subviews between both marker views until a `UIScrollView` instance (if any) is found.
3244

3345
> [!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 opt in for introspection (`.iOS(.vXYZ)`) because underlying UIKit/AppKit types can change between major versions.
3547
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`:
3749

3850
```swift
3951
ScrollView {
@@ -46,7 +58,7 @@ ScrollView {
4658

4759
### Usage in production
4860

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.
5062

5163
Install
5264
-------
@@ -55,14 +67,14 @@ Install
5567

5668
#### Xcode
5769

58-
<img width="656" src="https://github.com/siteline/swiftui-introspect/assets/2538074/d19c1dd3-9aa4-4e4f-a5a5-b2d6a5b9b927">
70+
<img width="660" height="300" src="https://github.com/user-attachments/assets/ab1c1a62-96d9-417d-ad2b-43012a69cae8" />
5971

6072
#### Package.swift
6173

6274
```swift
6375
let package = Package(
6476
dependencies: [
65-
.package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"),
77+
.package(url: "https://github.com/siteline/swiftui-introspect", from: "26.0.0"),
6678
],
6779
targets: [
6880
.target(name: <#Target Name#>, dependencies: [
@@ -75,11 +87,11 @@ let package = Package(
7587
### CocoaPods
7688

7789
```ruby
78-
pod 'SwiftUIIntrospect', '~> 1.0'
90+
pod 'SwiftUIIntrospect', '~> 26.0.0'
7991
```
8092

81-
Introspection
82-
-------------
93+
View Types
94+
----------
8395

8496
### Implemented
8597

@@ -132,6 +144,7 @@ Introspection
132144
- [`VideoPlayer`](https://swiftpackageindex.com/siteline/swiftui-introspect/main/documentation/swiftuiintrospect/videoplayertype)
133145
- [`View`](https://swiftpackageindex.com/siteline/swiftui-introspect/main/documentation/swiftuiintrospect/viewtype)
134146
- [`ViewController`](https://swiftpackageindex.com/siteline/swiftui-introspect/main/documentation/swiftuiintrospect/viewcontrollertype)
147+
- [`WebView`](https://swiftpackageindex.com/siteline/swiftui-introspect/main/documentation/swiftuiintrospect/webviewtype)
135148
- [`Window`](https://swiftpackageindex.com/siteline/swiftui-introspect/main/documentation/swiftuiintrospect/windowtype)
136149

137150
**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).
@@ -143,6 +156,18 @@ SwiftUI | Affected Frameworks | Why
143156
Text | UIKit, AppKit | Not a UILabel / NSLabel
144157
Image | UIKit, AppKit | Not a UIImageView / NSImageView
145158
Button | UIKit | Not a UIButton
159+
Link | UIKit, AppKit | Not a UIButton / NSButton
160+
NavigationLink | UIKit | Not a UIButton
161+
GroupBox | AppKit | No underlying view
162+
Menu | UIKit, AppKit | No underlying view
163+
Spacer | UIKit, AppKit | No underlying view
164+
Divider | UIKit, AppKit | No underlying view
165+
HStack, VStack, ZStack | UIKit, AppKit | No underlying view
166+
LazyVStack, LazyHStack, LazyVGrid, LazyHGrid | UIKit, AppKit | No underlying view
167+
Color | UIKit, AppKit | No underlying view
168+
ForEach | UIKit, AppKit | No underlying view
169+
GeometryReader | UIKit, AppKit | No underlying view
170+
Chart | UIKit, AppKit | Native SwiftUI framework
146171

147172
Examples
148173
--------
@@ -154,12 +179,10 @@ List {
154179
Text("Item")
155180
}
156181
.introspect(.list, on: .iOS(.v13, .v14, .v15)) { tableView in
157-
tableView.backgroundView = UIView()
158-
tableView.backgroundColor = .cyan
182+
tableView.bounces = false
159183
}
160184
.introspect(.list, on: .iOS(.v16, .v17, .v18, .v26)) { collectionView in
161-
collectionView.backgroundView = UIView()
162-
collectionView.subviews.dropFirst(1).first?.backgroundColor = .cyan
185+
collectionView.bounces = false
163186
}
164187
```
165188

@@ -170,7 +193,7 @@ ScrollView {
170193
Text("Item")
171194
}
172195
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) { scrollView in
173-
scrollView.backgroundColor = .red
196+
scrollView.bounces = false
174197
}
175198
```
176199

@@ -195,14 +218,32 @@ TextField("Text Field", text: <#Binding<String>#>)
195218
}
196219
```
197220

221+
General Guidelines
222+
------------------
223+
224+
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+
198233
Advanced usage
199234
--------------
200235

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+
201242
### Implement your own introspectable type
202243

203244
**Missing an element?** Please [start a discussion](https://github.com/siteline/swiftui-introspect/discussions/new?category=ideas).
204245

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.
206247

207248
For example, here's how the library implements the introspectable `TextField` type:
208249

@@ -257,9 +298,7 @@ extension macOSViewVersion<TextFieldType, NSTextField> {
257298

258299
### Introspect on future platform versions
259300

260-
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:
263302

264303
```swift
265304
import SwiftUI
@@ -277,11 +316,11 @@ struct ContentView: View {
277316
}
278317
```
279318

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.
281320

282321
### Keep instances outside the customize closure
283322

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:
285324

286325
```swift
287326
import SwiftUI
@@ -301,10 +340,21 @@ struct ContentView: View {
301340
}
302341
```
303342

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:
347+
348+
```swift
349+
.package(url: "https://github.com/siteline/swiftui-introspect", "1.3.0"..<"27.0.0"),
350+
```
351+
352+
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+
304354
Community projects
305355
------------------
306356

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:
308358

309359
<a href="https://github.com/paescebu/CustomKeyboardKit">
310360
<img src="https://github-readme-stats.vercel.app/api/pin/?username=paescebu&repo=CustomKeyboardKit" />

0 commit comments

Comments
 (0)