new-api-support
new-api-support
Add introspection support for a SwiftUI API (view type, modifier, or View extension function).
Usage
/new-api-support <entity_name>
Where <entity_name> is:
- A SwiftUI struct name (e.g.,
ContentUnavailableView,ProgressView) - A View extension function name (e.g.,
onAppear,disabled,opacity) - A modifier struct name (e.g.,
ScaledMetric)
Workflow
Step 1: Locate and Catalog the API
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:
- For functions: Find all overloads (same name, different parameters)
- For structs: Find the struct definition AND any View extension functions that return this type
- Note ALL
@availableattributes for each API variant
Example 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, *)
Step 2: Research Usage Context
Understand HOW the API is meant to be used by researching its typical context:
-
Search for documentation and usage patterns:
- Use web search to find Apple documentation and WWDC sessions
- Look for common usage patterns in tutorials and Stack Overflow
- Identify which parent views/containers the API is typically used with
-
Identify related parent/child relationships:
Entity Type Find Related Context View inside container Which container views typically hold this view? (e.g., Tabgoes insideTabView)View modifier Which views is this modifier typically applied to? (e.g., subscriptionStoreButtonLabelapplies toSubscriptionStoreView)Container-specific modifier Which container makes this modifier meaningful? (e.g., listRowBackgroundon views insideList)Style modifier Which view type does this style affect? (e.g., buttonStyleonButton) -
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:
- Some modifiers only work in specific contexts (e.g.,
listRowInsetsonly meaningful inList) - Some views only function inside specific parents (e.g.,
SectioninListorForm) - Test in the correct context to ensure real-world usability
- Some modifiers only work in specific contexts (e.g.,
Step 3: Determine File Placement
For new View types (structs like ProgressView, ContentUnavailableView):
- Create new file:
Sources/ViewInspector/SwiftUI/<ViewName>.swift - Create test file:
Tests/ViewInspectorTests/SwiftUI/<ViewName>Tests.swift
For 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
Step 4: Reverse Engineering Investigation
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:
- The internal modifier/view type name (e.g.,
_AppearanceActionModifier) - Property names and their paths (e.g.,
appear,disappear) - Nested structure for complex types
- Whether it uses
ModifiedContentwrapper
Example 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.
Step 5: Implement Introspection Support
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)
}
}
Step 6: Add Tests
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:
- If API is
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)→ test on all four platforms - If API is
@available(iOS 15.0, *)with@available(macOS, unavailable)→ only test iOS - watchOS is peculiar: always use
.watchOS/watchOS.xcodeprojinstead of the main package
Quick platform matrix check:
# List available simulators
xcrun simctl list devices available
Step 7: Update readiness.md
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.
Step 8: Update unsupported_swiftui_apis.md
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:
- For view types: Remove from "View Types - Not Supported" or "View Types - Partial Support"
- For modifiers: Remove from "View Modifiers - Not Supported"
- For property wrappers: Remove from "Property Wrappers - Not Supported"
- For partial support completions: Remove from "View Types - Partial Support" if now fully supported
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.
Important Notes
-
Always check
@availableattributes - Copy them exactly from the SDK and apply to all introspection code and tests -
Use
XCTAssertThrowsinstead ofXCTAssertThrowsError(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)Modifiedsuffix: 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.swiftis needed forfind()to work
Public API Design Principles
-
Never return
Anyfrom public APIs - Consumers need typed values they can work with -
Prefer returning SwiftUI types over
String,Any, or custom types:- If a public SwiftUI type exists (like
GlassEffectTransition), return it - When internal types (e.g.,
_GlassEffectTransition) can't be cast to public types, investigate if you can map string descriptions to public type values
- If a public SwiftUI type exists (like
-
Create wrapper types for complex modifier parameters:
- Instead of multiple methods like
glassEffectTintColor()andglassEffectShape(), create a wrapperViewType.GlassEffectwith methodstintColor(),shape(), etc. - The wrapper holds the internal config and provides typed accessors
- Example pattern:
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 { ... } } }- Note: When inside ViewType namespace, use fully qualified names like
SwiftUI.ColorandSwiftUI.Shapeto avoid conflicts with other ViewType members
- Instead of multiple methods like
-
Add
BinaryEquatableconformance to the public library API (not just tests) for SwiftUI types that consumers may want to compare in their tests
Investigation Techniques
-
Inspector.print()returns a String - Must wrap withprint()to see output:print(Inspector.print(someValue)) // Correct Inspector.print(someValue) // Wrong - output not visible -
Investigate chained method calls to understand behavior:
- Chaining same method (e.g.,
.tint(.red).tint(.blue)) typically overwrites - last value wins - Chaining different methods (e.g.,
.tint(.red).interactive(true)) preserves all values - There's usually no internal "combined" structure - just single values per property
- Chaining same method (e.g.,
-
Check type casting between internal and public types:
- Internal types like
_Glassoften cannot be cast to public types likeGlass - When casting fails, extract individual properties and map them to public type values
- Internal types like
Test Best Practices
-
Test in proper context - Always test views and modifiers in their intended usage context:
- Child views should be tested inside their parent containers (e.g.,
TabinsideTabView) - Container-specific modifiers should be tested on views inside that container (e.g.,
listRowBackgroundon views insideList) - Style modifiers should be tested on the view type they style (e.g.,
buttonStyleonButton) - This ensures tests validate real-world usability, not just technical extraction
- Child views should be tested inside their parent containers (e.g.,
-
Platform-unavailable APIs - Wrap entire test file in
#if !os(visionOS)(or appropriate platform), not individual tests -
Add
@MainActorto 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
XCTAssertNotNilwhen 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.IDis recreated during inspection; useXCTAssertNotNilwith a comment explaining why
Advanced Introspection Techniques
ViewInspector uses several advanced techniques to access SwiftUI's internal structures. Use these when standard Inspector.attribute() approaches don't work.
1. BinaryEquatable Protocol
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.
2. unsafeBitCast with Allocator Pattern
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, Int64, Int64) = (0, 0, 0, 0, 0, 0)
}
struct Allocator52 {
let data: (Allocator48, Int32) = (.init(), 0)
}
init() {
switch MemoryLayout<Self>.size {
case 52:
self = unsafeBitCast(Allocator52(), to: GeometryProxy.self)
case 48:
self = unsafeBitCast(Allocator48(), to: GeometryProxy.self)
default:
fatalError(MemoryLayout<Self>.actualSize())
}
}
}
Key patterns:
- Create multiple Allocator variants for different OS versions (struct sizes change!)
- Use
MemoryLayout<Self>.sizeswitch to pick the right allocator - Use
MemoryLayout<Self>.actualSize()in default case to discover new sizes - Name allocators by size (e.g.,
Allocator48,Allocator52)
When to use: Creating gesture values, style configurations, proxies, _ViewModifier_Content, _PreferenceValue, etc.
3. Surrogate Pattern for Private Struct Access
Problem: Need to access internal properties of a private SwiftUI type like SignInWithAppleButton.
Solution: Create a Surrogate struct matching the memory layout, then rebind:
// The Surrogate mirrors the internal structure
extension SignInWithAppleButton {
struct Surrogate {
let type: ASAuthorizationAppleIDButton.ButtonType
let onRequest: (ASAuthorizationAppleIDRequest) -> Void
let onCompletion: (Result<ASAuthorization, Error>) -> Void
}
}
// Access via unsafeMemoryRebind
private func buttonSurrogate() throws -> Surrogate {
let button = try Inspector.cast(value: content.view, type: SignInWithAppleButton.self)
return try Inspector.unsafeMemoryRebind(value: button, type: Surrogate.self)
}
When to use: When you need to access multiple internal properties or invoke internal callbacks.
4. unsafeMemoryRebind for Type Conversion
Problem: Need to convert between types with identical memory layouts (e.g., Swift 6 Sendable closures).
Solution: Use Inspector.unsafeMemoryRebind:
// In Inspector.swift
static func unsafeMemoryRebind<V, T>(value: V, type: T.Type) throws -> T {
guard MemoryLayout<V>.size == MemoryLayout<T>.size else {
throw InspectionError.notSupported("Unable to rebind...")
}
return withUnsafeBytes(of: value) { bytes in
return bytes.baseAddress!
.assumingMemoryBound(to: T.self).pointee
}
}
When to use: Converting closure types with different Sendable annotations, or accessing surrogate types.
5. withUnsafePointer + withMemoryRebound for Binding Conversion
Problem: Internal enum type is wrapped in Binding but you need a different Binding type (e.g., Toggle's internal ToggleState enum).
Solution: Rebind the pointer:
// Toggle changed from Binding<Bool> to Binding<ToggleState> in iOS 16
private enum ToggleState { case on, off, mixed }
let toggleStateBinding = try Inspector.attribute(label: "_toggleState", value: content.view)
let toggleState = withUnsafePointer(to: toggleStateBinding) {
$0.withMemoryRebound(to: Binding<ToggleState>.self, capacity: 1) {
$0.pointee
}
}
// Now create a Bool binding that wraps the ToggleState
return Binding(
get: { toggleState.wrappedValue == .on },
set: { toggleState.wrappedValue = $0 ? .on : .off }
)
When to use: When the internal binding type differs from the public API type.
6. withUnsafeBytes + bindMemory for Closure Invocation
Problem: Need to invoke a closure stored in a generic container with specific type parameters.
Solution: Bind the closure's memory to the expected type:
typealias Closure = (EnvironmentValues) -> ModifiedContent<V, SomeModifier>
guard let typedClosure = withUnsafeBytes(of: closure, {
$0.bindMemory(to: Closure.self).first
}) else { throw InspectionError.typeMismatch(closure, Closure.self) }
let view = typedClosure(EnvironmentValues())
When to use: Invoking closure-based content builders like GeometryReader, ScrollViewReader, or navigationBarItems.
7. Memory Patching for Environment Injection
Problem: Need to inject environment objects into views that require them for inspection.
Solution: Scan memory for matching patterns and patch:
// In EnvironmentInjection.swift
static func inject<T>(environmentObject: AnyObject, into entity: T) -> T {
let envObjSize = EnvObject.structSize
var offset = MemoryLayout<T>.stride - envObjSize
return withUnsafeBytes(of: EnvObject.Forgery(object: nil)) { reference in
while offset >= 0 {
var copy = entity
withUnsafeMutableBytes(of: ©) { bytes in
// Check if memory at offset matches empty env object pattern
guard bytes[offset..<offset + envObjSize].elementsEqual(reference)
else { return }
// Write marker to verify location
let rawPointer = bytes.baseAddress! + offset + EnvObject.seedOffset
let pointerToValue = rawPointer.assumingMemoryBound(to: Int.self)
pointerToValue.pointee = -1
}
// Verify via reflection that we found the right location
if let seed = try? Inspector.attribute(path: label + "|_seed", value: copy, type: Int.self),
seed == -1 {
// Patch with actual object
withUnsafeMutableBytes(of: ©) { bytes in
let rawPointer = bytes.baseAddress! + offset
let pointerToValue = rawPointer.assumingMemoryBound(to: EnvObject.Forgery.self)
pointerToValue.pointee = .init(object: environmentObject)
}
return copy
}
offset -= step
}
return entity
}
}
When to use: This technique is already implemented in ViewInspector for environment injection. Reference it when debugging environment-related issues.
8. NSKeyedUnarchiver + setValue for Objective-C Types
Problem: Need to create instances of Objective-C types without public initializers (e.g., ASAuthorizationAppleIDCredential).
Solution: Decode from archived data and mutate with setValue:
public extension ASAuthorizationAppleIDCredential {
convenience init(user: String, email: String?, fullName: PersonNameComponents?, ...) {
// Base64-encoded archived empty instance
let data = Data(base64Encoded: "YnBsaXN0MDD...")
let decoder = try! NSKeyedUnarchiver(forReadingFrom: data!)
self.init(coder: decoder)!
// Mutate with desired values
setValue(user, forKey: "user")
setValue(email, forKey: "email")
setValue(fullName, forKey: "fullName")
// ... more properties
}
}
When to use: Creating Objective-C class instances that need custom initialization for testing.
9. MemoryLayout.actualSize() Discovery Helper
Problem: SwiftUI struct sizes change between OS versions, breaking Allocator patterns.
Solution: Use a helper to discover new sizes:
internal extension MemoryLayout {
static func actualSize() -> String {
fatalError("New size of \(String(describing: type(of: T.self))) is \(Self.size)")
}
}
// Usage in switch default case:
init(fractionCompleted: Double?) {
switch MemoryLayout<Self>.size {
case 12:
self = unsafeBitCast(Allocator12(fractionCompleted: fractionCompleted), to: Self.self)
case 36:
self = unsafeBitCast(Allocator36(fractionCompleted: fractionCompleted), to: Self.self)
default:
fatalError(MemoryLayout<Self>.actualSize()) // "New size of ProgressViewStyleConfiguration is 48"
}
}
When to use: Always include in Allocator pattern switch statements to detect OS changes.
10. String Description Mapping for Internal Enums
Problem: Internal enums can't be cast to public types, but their string description matches public values.
Solution: Map string descriptions to public enum values:
func glassEffectTransition() throws -> GlassEffectTransition {
let kind = try modifierAttribute(
modifierName: "GlassEffectTransitionModifier", path: "modifier|transition|kind",
type: Any.self, call: "glassEffectTransition")
// Internal _GlassEffectTransition can't cast to GlassEffectTransition
// but String(describing:) reveals the case name
let description = String(describing: kind)
if description == "materialize" {
return .materialize
} else if description.hasPrefix("matchedGeometry") {
return .matchedGeometry
} else {
return .identity
}
}
When to use: When internal enums have public equivalents but casting fails.
Quick Reference: Choosing the Right Technique
| Problem | Technique |
|---|---|
| Compare non-Equatable types | BinaryEquatable |
| Create type without public init | Allocator + unsafeBitCast |
| Access private struct properties | Surrogate + unsafeMemoryRebind |
| Convert closure types | withUnsafeBytes + bindMemory |
| Convert binding types | withUnsafePointer + withMemoryRebound |
| Create Obj-C types | NSKeyedUnarchiver + setValue |
| Handle OS version changes | MemoryLayout switch + actualSize() |
| Map internal to public enum | String(describing:) mapping |