SwiftUI Introspect

repository·main·Indexed 26 days ago

https://github.com/siteline/swiftui-introspect

A library that provides a way to access the underlying UIKit or AppKit views for SwiftUI components, allowing developers to use APIs not yet exposed by SwiftUI. It features the .introspect modifier for accessing views or ancestors, support for custom introspectable types via the IntrospectableViewType protocol, and a @Weak property wrapper to avoid retain cycles. Supported platforms include iOS, Mac Catalyst, macOS, tvOS, and visionOS.

Tokens
1.8K
Snippets
7
Records
8
Agent score
42%

What's inside SwiftUI Introspect

  1. Use the .introspect modifier to access underlying views

    main

    The .introspect modifier allows you to access the underlying UIKit or AppKit view for a SwiftUI view.

    Important Requirements:

    • Explicit Opt-in: You must specify the platform and version (e.g., .iOS(.v17, .v18)) because underlying types can change between major OS versions.
    • Scope: By default, .introspect acts on its receiver. To introspect an ancestor view instead, set scope: .ancestor.

    Best Practices:

    • Use sparingly: Prefer native SwiftUI modifiers when available.
    • Program defensively: The closure may be called multiple times during the view lifecycle; ensure your code handles re-execution without side effects.
    • Avoid direct state changes: Do not change SwiftUI state inside the closure. If necessary, wrap updates in DispatchQueue.main.async.
    • Avoid retain cycles: Use [weak self] when capturing references.
    ScrollView {
    	Text("Item 1")
    }
    .introspect(.scrollView, on: .iOS(.v17, .v18, .v26, .v27)) { scrollView in
    	// do something with UIScrollView
    }
  2. Configure SwiftUI Introspect for Xcode Multiplatform targets

    main

    When using an Xcode multiplatform target, you must filter the package product dependency to exclude watchOS.

    1. Select the SwiftUIIntrospect package product dependency in your project settings.
    2. Set the platform filter to include: iOS, Mac Catalyst, macOS, tvOS, and visionOS.
    3. Ensure watchOS is unchecked.

    Always use #if canImport(SwiftUIIntrospect) guards around imports and introspection logic in shared source files to maintain compatibility with watchOS builds.

  3. Configure SwiftUI Introspect for multi-platform Swift Package targets

    main

    SwiftUI Introspect supports iOS, Mac Catalyst, macOS, tvOS, and visionOS, but it does not support watchOS.

    If your Swift Package target supports multiple platforms, use a platform condition in your Package.swift to ensure SwiftUIIntrospect is only included for supported platforms. This prevents build errors when targeting watchOS.

    To ensure your source code compiles on platforms where the library is not linked (like watchOS), wrap your imports and introspection calls in #if canImport(SwiftUIIntrospect) guards.

    // In Package.swift
    .target(
    	name: "AppFeature",
    	dependencies: [
    		.product(
    			name: "SwiftUIIntrospect",
    			package: "swiftui-introspect",
    			condition: .when(platforms: [.iOS, .macCatalyst, .macOS, .tvOS, .visionOS])
    		),
    	]
    )
    
    // In your Swift source files
    #if canImport(SwiftUIIntrospect)
    import SwiftUIIntrospect
    #endif
    
    struct ContentView: View {
    	var body: some View {
    		ScrollView {
    			Text("Item")
    		}
    		#if canImport(SwiftUIIntrospect)
    		.introspect(.scrollView, on: .iOS(.v17, .v18, .v26, .v27)) { scrollView in
    			scrollView.bounces = false
    		}
    		#endif
    	}
    }
  4. Install SwiftUIIntrospect via Swift Package Manager

    main

    Add SwiftUIIntrospect to your Swift Package Manager dependencies using the following URL and version range:

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

    Then, add the product to your target:

    .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
    .package(url: "https://github.com/siteline/swiftui-introspect", from: "27.0.0-beta"),
    .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
  5. Introspect an ancestor view using scope: .ancestor

    main

    If you need to access a view that is an ancestor of the current view rather than the receiver itself, use the scope: .ancestor parameter.

    ScrollView {
    	Text("Item 1")
    		.introspect(.scrollView, on: .iOS(.v17, .v18, .v26, .v27), scope: .ancestor) { scrollView in
    			// do something with UIScrollView
    	}
    }
  6. Introspect on future platform versions using range-based predicates

    main

    To cover future OS versions without manual updates, you can use range-based version predicates. This requires importing the library via @_spi(Advanced).

    Warning: Use this cautiously. If a future OS version changes the underlying UIKit/AppKit type, the customization closure will not run unless support is explicitly declared.

    import SwiftUI
    @_spi(Advanced) import SwiftUIIntrospect
    
    struct ContentView: View {
    	var body: some View {
    		ScrollView {
    			// ...
    		}
    		.introspect(.scrollView, on: .iOS(.v13...)) { scrollView in
    			// ...
    		}
    	}
    }
  7. Implement a custom introspectable type

    main

    If a view type is not supported, you can implement your own using the IntrospectableViewType protocol. This requires importing the library using the @_spi(Advanced) attribute.

    To implement a type, you must:

    1. Define a struct conforming to IntrospectableViewType.
    2. Extend IntrospectableViewType to provide a static property for the type.
    3. Extend the platform-specific version types (e.g., iOSViewVersion, macOSViewVersion) to include your new 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 v17 = Self(for: .v17)
    	// ... other versions
    }
    #endif
  8. Store introspected instances using @Weak

    main

    To keep an introspected instance (like a UIScrollView) outside the customization closure without creating retain cycles, use the @Weak property wrapper. This requires importing the library via @_spi(Advanced). Do not use @State for this purpose.

    import SwiftUI
    @_spi(Advanced) import SwiftUIIntrospect
    
    struct ContentView: View {
    	@Weak var scrollView: UIScrollView?
    
    	var body: some View {
    		ScrollView {
    			// ...
    		}
    		.introspect(.scrollView, on: .iOS(.v17, .v18, .v26, .v27)) { scrollView in
    			self.scrollView = scrollView
    		}
    	}
    }