Add introspection support for a SwiftUI API (view type, modifier, or View extension function). Use when the user wants to add support for a new SwiftUI entity to ViewInspector.
Add introspection support for a SwiftUI API (view type, modifier, or View extension function).
/new-api-support <entity_name>
Where <entity_name> is:
ContentUnavailableView, ProgressView)onAppear, disabled, opacity)ScaledMetric)Find the API in the local iOS SDK (prefer local over network):
# Find SwiftUI interface files in Xcode SDK
find /Applications/Xcode.app/Contents/Developer/Platforms -name "SwiftUI.swiftmodule" -type d 2>/dev/null | head -5
# Search for the entity in SwiftUI interfaces
grep -r "<entity_name>" /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/*.swiftinterface 2>/dev/null | head -50
# For macOS SDK
grep -r "<entity_name>" /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/*.swiftinterface 2>/dev/null | head -50
Catalog ALL related APIs:
@available attributes for each API variantExample catalog format:
Entity: .buttonStyle(_:)
Type: View extension function
Related APIs:
1. func buttonStyle<S>(_ style: S) -> some View where S : ButtonStyle
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
2. func buttonStyle<S>(_ style: S) -> some View where S : PrimitiveButtonStyle
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
Understand HOW the API is meant to be used by researching its typical context:
Search for documentation and usage patterns:
Identify related parent/child relationships:
| Entity Type | Find Related Context |
|-------------|---------------------|
| View inside container | Which container views typically hold this view? (e.g., Tab goes inside TabView) |
| View modifier | Which views is this modifier typically applied to? (e.g., subscriptionStoreButtonLabel applies to SubscriptionStoreView) |
| Container-specific modifier | Which container makes this modifier meaningful? (e.g., listRowBackground on views inside List) |
| Style modifier | Which view type does this style affect? (e.g., buttonStyle on Button) |
Document the context for test design:
Example context analysis:
Entity: Tab
Type: View
Typical Context: Used as direct child of TabView
Related APIs: TabView, tabItem (deprecated predecessor)
Test Structure: Tab should be tested INSIDE TabView hierarchy
Entity: subscriptionStoreButtonLabel
Type: View modifier
Typical Context: Applied to SubscriptionStoreView
Related APIs: SubscriptionStoreView, SubscriptionStoreButton
Test Structure: Apply modifier to SubscriptionStoreView, not EmptyView
Check for container-dependent behavior:
listRowInsets only meaningful in List)Section in List or Form)For new View types (structs like ProgressView, ContentUnavailableView):
Sources/ViewInspector/SwiftUI/<ViewName>.swiftTests/ViewInspectorTests/SwiftUI/<ViewName>Tests.swiftFor View modifiers/functions, find the appropriate existing file by category:
| Category | Source File | Test File |
|----------|-------------|-----------|
| Animation (.animation, .transition) | Modifiers/AnimationModifiers.swift | ViewModifiers/AnimationModifiersTests.swift |
| Configuration (.disabled, .labelsHidden) | Modifiers/ConfigurationModifiers.swift | ViewModifiers/ConfigurationModifiersTests.swift |
| Environment (.environment, .environmentObject) | Modifiers/EnvironmentModifiers.swift | ViewModifiers/EnvironmentModifiersTests.swift |
| Interaction (.onTapGesture, .onAppear) | Modifiers/InteractionModifiers.swift | ViewModifiers/InteractionModifiersTests.swift |
| Positioning (.offset, .position) | Modifiers/PositioningModifiers.swift | ViewModifiers/PositioningModifiersTests.swift |
| Sizing (.frame, .fixedSize) | Modifiers/SizingModifiers.swift | ViewModifiers/SizingModifiersTests.swift |
| Text input (.keyboardType, .textContentType) | Modifiers/TextInputModifiers.swift | ViewModifiers/TextInputModifiersTests.swift |
| Transform (.rotationEffect, .scaleEffect) | Modifiers/TransformingModifiers.swift | ViewModifiers/TransformingModifiersTests.swift |
| Navigation bar (.navigationTitle) | Modifiers/NavigationBarModifiers.swift | - |
| Custom styles (.buttonStyle, .pickerStyle) | Modifiers/CustomStyleModifiers.swift | - |
Check existing files to confirm the pattern:
grep -l "similar_modifier" Sources/ViewInspector/Modifiers/*.swift
Create a reverse engineering test to understand the internal structure:
import XCTest
import SwiftUI
@testable import ViewInspector
final class ReverseEngineeringTests: XCTestCase {
func testInvestigate_<EntityName>() throws {
// Create a simple view using the target API
let sut = EmptyView().<targetAPI>()
// Print the internal structure
print("\(Inspector.print(sut) as AnyObject)")
}
}
Run the investigation test:
swift test --filter "testInvestigate_"
Analyze the output to identify:
_AppearanceActionModifier)appear, disappear)ModifiedContent wrapperExample Inspector.print output:
EmptyView
→ _AppearanceActionModifier
modifier: _AppearanceActionModifier
appear: Optional<() -> ()>
some: (Function)
disappear: Optional<() -> ()>
none
Iterate investigation for each API variant and parameter combination to understand all internal structures.
For View modifiers, add to the appropriate Modifiers file:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public extension InspectableView {
// For simple value extraction
func <modifierName>() throws -> <ReturnType> {
return try modifierAttribute(
modifierName: "<InternalModifierName>", // From Inspector.print
path: "modifier|<propertyPath>", // Path to the value
type: <ReturnType>.self,
call: "<modifierName>")
}
// For callback invocation
func call<CallbackName>() throws {
let callback = try modifierAttribute(
modifierName: "<InternalModifierName>",
path: "modifier|<callbackPath>",
type: (() -> Void).self,
call: "call<CallbackName>")
callback()
}
}
For new View types, create the full ViewType structure:
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension ViewType {
struct NewViewType: KnownViewType {
public static let typePrefix: String = "NewViewType" // From Inspector.print
public static var namespacedPrefixes: [String] {
["SwiftUI.NewViewType"]
}
}
}
// MARK: - Content Extraction
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension ViewType.NewViewType: SingleViewContent { // or MultipleViewContent
public static func child(_ content: Content) throws -> Content {
return try Inspector.attribute(path: "content", value: content.view)
}
}
// MARK: - Extraction from View hierarchy
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView where View == ViewType.NewViewType {
// Add attribute getters based on Inspector.print analysis
func someAttribute() throws -> SomeType {
return try Inspector.attribute(
path: "attributePath",
value: content.view,
type: SomeType.self)
}
}
// MARK: - Global View hierarchy access
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public extension InspectableView {
func newViewType(_ index: Int? = nil) throws -> InspectableView<ViewType.NewViewType> {
return try contentForModifierLookup.newViewType(parent: self, index: index)
}
}
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
internal extension Content {
func newViewType(parent: UnwrappedView, index: Int?) throws
-> InspectableView<ViewType.NewViewType> {
let call = "newViewType(\(index == nil ? "" : "\(index!)"))"
return try .init(try Inspector.attribute(path: "content", value: view),
parent: parent, call: call, index: index)
}
}
IMPORTANT: Use contextual test structure based on Step 2 research.
Tests should reflect real-world usage patterns, not just technical functionality.
For views that belong inside specific containers:
import XCTest
import SwiftUI
@testable import ViewInspector
// Example: Tab is meant to be used inside TabView
@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
final class TabTests: XCTestCase {
// Test extraction in proper context (inside TabView)
func testTabInsideTabView() throws {
let sut = TabView {
Tab("Home", systemImage: "house") {
Text("Home Content")
}
Tab("Settings", systemImage: "gear") {
Text("Settings Content")
}
}
let tab = try sut.inspect().tabView().tab(0)
XCTAssertEqual(try tab.labelView().text().string(), "Home")
}
// Test searching for content inside Tab inside TabView
func testSearchForContentInsideTab() throws {
let sut = TabView {
Tab("Home", systemImage: "house") {
Text("Home Content")
}
}
XCTAssertEqual(
try sut.inspect().find(text: "Home Content").pathToRoot,
"tabView().tab(0).text()")
}
}
For modifiers that apply to specific view types:
// Example: subscriptionStoreButtonLabel applies to SubscriptionStoreView
@available(iOS 17.0, macOS 14.0, *)
final class SubscriptionModifiersTests: XCTestCase {
func testSubscriptionStoreButtonLabel() throws {
let sut = SubscriptionStoreView(productIDs: ["com.app.subscription"])
.subscriptionStoreButtonLabel(.multiline)
let label = try sut.inspect().subscriptionStoreView().subscriptionStoreButtonLabel()
XCTAssertEqual(label, .multiline)
}
}
// Example: listRowBackground applies to views inside List
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
final class ListModifiersTests: XCTestCase {
func testListRowBackgroundInsideList() throws {
let sut = List {
Text("Row")
.listRowBackground(Color.red)
}
let background = try sut.inspect().list().text(0).listRowBackground()
// Verify the background color
}
}
For style modifiers, test with the view type they style:
// Example: buttonStyle applies to Button
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
final class ButtonStyleTests: XCTestCase {
func testButtonStyleOnButton() throws {
let sut = Button("Tap") { }
.buttonStyle(.borderedProminent)
let style = try sut.inspect().button().buttonStyle()
// Verify style properties
}
}
Generic test file structure (when no specific context applies):
import XCTest
import SwiftUI
@testable import ViewInspector
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
final class NewViewTypeTests: XCTestCase {
// Test basic extraction
func testExtractionFromSingleViewContainer() throws {
let view = AnyView(NewViewType())
XCTAssertNoThrow(try view.inspect().anyView().newViewType())
}
// Test attribute inspection
func testSomeAttributeInspection() throws {
let sut = NewViewType(someParam: .value)
let value = try sut.inspect().newViewType().someAttribute()
XCTAssertEqual(value, .value)
}
// Test in view hierarchy
func testSearch() throws {
let view = HStack { NewViewType() }
XCTAssertEqual(try view.inspect().find(ViewType.NewViewType.self).pathToRoot,
"hStack().newViewType(0)")
}
}
For generic modifiers (not container-specific):
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
final class SomeModifierTests: XCTestCase {
func testModifierApplication() throws {
let sut = EmptyView().someModifier(value: 42)
XCTAssertNoThrow(try sut.inspect().emptyView())
}
func testModifierValueInspection() throws {
let sut = EmptyView().someModifier(value: 42)
let value = try sut.inspect().emptyView().someModifier()
XCTAssertEqual(value, 42)
}
}
Run tests incrementally (headless - fast iteration):
# Run specific test
swift test --filter "NewViewTypeTests/testExtractionFromSingleViewContainer"
# Run all tests for the new type
swift test --filter "NewViewTypeTests"
Final verification on platform simulators:
Headless swift test is fast and suitable for development iteration, but final verification must run on actual platform simulators for all platforms the API supports.
Based on the API's @available attributes, verify tests pass on each supported platform:
# iOS Simulator
xcodebuild test -scheme ViewInspector -destination 'platform=iOS Simulator,name=iPhone 16'
# tvOS Simulator
xcodebuild test -scheme ViewInspector -destination 'platform=tvOS Simulator,name=Apple TV'
# macOS
xcodebuild test -scheme ViewInspector -destination 'platform=macOS'
# visionOS Simulator
xcodebuild test -scheme ViewInspector -destination 'platform=visionOS Simulator,name=Apple Vision Pro'
# watchOS - SPECIAL: Uses separate Xcode project
xcodebuild test -project .watchOS/watchOS.xcodeproj -scheme watchOS -destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)'
Platform testing requirements:
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) → test on all four platforms@available(iOS 15.0, *) with @available(macOS, unavailable) → only test iOS.watchOS/watchOS.xcodeproj instead of the main packageQuick platform matrix check:
# List available simulators
xcrun simctl list devices available
Add the new API to readiness.md in the appropriate section:
For View types, add to the "View Types" table:
|:white_check_mark:| NewViewType | `attribute1`, `attribute2`, `containedView` |
For Modifiers, add to the "View Modifiers" table:
|:white_check_mark:| `.someModifier(value:)` | `someModifier() -> Type` |
Maintain alphabetical order within each section.
After adding support for a new API, check if unsupported_swiftui_apis.md exists in the repository root and remove the entry for the newly supported API:
# Check if the file exists
ls unsupported_swiftui_apis.md
# Search for the API entry
grep -n "<entity_name>" unsupported_swiftui_apis.md
Remove the entry from the appropriate section:
Also remove from Quick Reference if the API was listed there:
# Remove lines like:
/new-api-support Gauge
/new-api-support "Group(subviews:)"
If the file doesn't exist, skip this step.
Always check @available attributes - Copy them exactly from the SDK and apply to all introspection code and tests
Use XCTAssertThrows instead of XCTAssertThrowsError (enforced by SwiftLint)
Handle platform differences - Some APIs have different availability on iOS/macOS/tvOS/watchOS. Use #if os() when needed
Test all overloads - Each function variant may have different internal structure
Modifier path format - Use | as path separator: "modifier|property|nestedProperty"
Common internal type prefixes:
_ prefix: Internal SwiftUI types (e.g., _AppearanceActionModifier)Modified suffix: Wrapped content (e.g., ModifiedContent)For callbacks/closures - Store them via try Inspector.attribute() then invoke
Registration in ViewSearchIndex - For new view types, check if registration in ViewSearchIndex.swift is needed for find() to work
Never return Any from public APIs - Consumers need typed values they can work with
Prefer returning SwiftUI types over String, Any, or custom types:
GlassEffectTransition), return it_GlassEffectTransition) can't be cast to public types, investigate if you can map string descriptions to public type valuesCreate wrapper types for complex modifier parameters:
glassEffectTintColor() and glassEffectShape(), create a wrapper ViewType.GlassEffect with methods tintColor(), shape(), etc.public extension ViewType {
struct GlassEffect {
private let config: Any
public func tintColor() throws -> SwiftUI.Color? { ... }
public func shape<S>(_ type: S.Type) throws -> S where S: SwiftUI.Shape { ... }
}
}
SwiftUI.Color and SwiftUI.Shape to avoid conflicts with other ViewType membersAdd BinaryEquatable conformance to the public library API (not just tests) for SwiftUI types that consumers may want to compare in their tests
Inspector.print() returns a String - Must wrap with print() to see output:
print(Inspector.print(someValue)) // Correct
Inspector.print(someValue) // Wrong - output not visible
Investigate chained method calls to understand behavior:
.tint(.red).tint(.blue)) typically overwrites - last value wins.tint(.red).interactive(true)) preserves all valuesCheck type casting between internal and public types:
_Glass often cannot be cast to public types like GlassTest in proper context - Always test views and modifiers in their intended usage context:
Tab inside TabView)listRowBackground on views inside List)buttonStyle on Button)Platform-unavailable APIs - Wrap entire test file in #if !os(visionOS) (or appropriate platform), not individual tests
Add @MainActor to test class to avoid main actor isolation warnings
Don't test platform-specific default values - They may vary across platforms. Only test explicit parameter values
Compare to exact values instead of XCTAssertNotNil when possible:
// Good
XCTAssertEqual(result.id, AnyHashable("testID"))
// Avoid when exact comparison is possible
XCTAssertNotNil(result.id)
Don't duplicate tests that test the same functionality with different values (e.g., don't need separate tests for Circle, Capsule, RoundedRectangle shapes - one is sufficient)
Test missing modifier errors - Verify error messages are correct:
func testGlassEffectMissingModifierError() throws {
let sut = EmptyView().padding()
XCTAssertThrows(
try sut.inspect().emptyView().glassEffect(),
"EmptyView does not have 'glassEffect' modifier")
}
Search tests should search for child views inside the container, not just the container itself:
func testSearchForChildInsideContainer() throws {
let view = VStack {
NewContainer {
Text("Child1")
Text("Child2")
}
}
XCTAssertEqual(
try view.inspect().find(text: "Child2").pathToRoot,
"vStack().newContainer(0).text(1)")
}
Some values can't be compared directly - Namespace.ID is recreated during inspection; use XCTAssertNotNil with a comment explaining why
ViewInspector uses several advanced techniques to access SwiftUI's internal structures. Use these when standard Inspector.attribute() approaches don't work.
Problem: SwiftUI types like Font.Weight, ToolbarItemPlacement, GlassEffectTransition don't conform to Equatable, making test assertions impossible.
Solution: BinaryEquatable compares raw memory bytes instead:
// In BaseTypes.swift
public protocol BinaryEquatable: Equatable { }
extension BinaryEquatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
withUnsafeBytes(of: lhs) { lhsBytes -> Bool in
withUnsafeBytes(of: rhs) { rhsBytes -> Bool in
lhsBytes.elementsEqual(rhsBytes)
}
}
}
}
// Usage - add conformance to enable comparisons
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension ToolbarItemPlacement: BinaryEquatable { }
@available(iOS 26.0, macOS 26.0, tvOS 26.0, watchOS 26.0, *)
extension GlassEffectTransition: BinaryEquatable { }
When to use: For any SwiftUI type that needs equality comparison in tests but doesn't provide Equatable.
Problem: Many SwiftUI types (gesture values, style configurations, proxies) don't have public initializers, but tests need to create instances.
Solution: Create an Allocator struct matching the memory layout, then bitcast:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
private extension GeometryProxy {
struct Allocator48 {
let data: (Int64, Int64, Int64, Int64,
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer