Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ jobs:
runtime: macOS 14
os: macos-14
xcode: 15.4
- platform: [macOS, 15]
runtime: macOS 15
os: macos-15
xcode: 16.2

- platform: [visionOS, 1]
runtime: visionOS 1.2
Expand Down
1 change: 0 additions & 1 deletion .spi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ version: 1
builder:
configs:
- documentation_targets: [SwiftUIIntrospect]
custom_documentation_parameters: [--include-extended-types]
42 changes: 21 additions & 21 deletions Examples/Showcase/Showcase/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct AppView: View {
window.backgroundColor = .brown
}
#elseif os(macOS)
.introspect(.window, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { window in
.introspect(.window, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { window in
window.backgroundColor = .lightGray
}
#endif
Expand Down Expand Up @@ -92,7 +92,7 @@ struct ListShowcase: View {
collectionView.subviews.dropFirst(1).first?.backgroundColor = .cyan
}
#elseif os(macOS)
.introspect(.list, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { tableView in
.introspect(.list, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { tableView in
tableView.backgroundColor = .cyan
}
#endif
Expand All @@ -117,7 +117,7 @@ struct ListShowcase: View {
collectionView.subviews.dropFirst(1).first?.backgroundColor = .cyan
}
#elseif os(macOS)
.introspect(.list, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), scope: .ancestor) { tableView in
.introspect(.list, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), scope: .ancestor) { tableView in
tableView.backgroundColor = .cyan
}
#endif
Expand Down Expand Up @@ -155,7 +155,7 @@ struct ScrollViewShowcase: View {
scrollView.layer.backgroundColor = UIColor.cyan.cgColor
}
#elseif os(macOS)
.introspect(.scrollView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { scrollView in
.introspect(.scrollView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { scrollView in
scrollView.drawsBackground = true
scrollView.backgroundColor = .cyan
}
Expand All @@ -177,7 +177,7 @@ struct ScrollViewShowcase: View {
scrollView.layer.backgroundColor = UIColor.cyan.cgColor
}
#elseif os(macOS)
.introspect(.scrollView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), scope: .ancestor) { scrollView in
.introspect(.scrollView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), scope: .ancestor) { scrollView in
scrollView.drawsBackground = true
scrollView.backgroundColor = .cyan
}
Expand Down Expand Up @@ -310,7 +310,7 @@ struct GenericViewShowcase: View {
view.backgroundColor = .cyan
}
#elseif os(macOS)
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { view in
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { view in
view.layer?.backgroundColor = NSColor.cyan.cgColor
}
#endif
Expand All @@ -322,11 +322,11 @@ struct GenericViewShowcase: View {
.view,
on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18), .tvOS(.v13, .v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2)
) { view in
view.backgroundColor = .lightGray
view.backgroundColor = .yellow
}
#elseif os(macOS)
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { view in
view.layer?.backgroundColor = NSColor.lightGray.cgColor
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { view in
view.layer?.backgroundColor = NSColor.yellow.cgColor
}
#endif

Expand All @@ -339,7 +339,7 @@ struct GenericViewShowcase: View {
view.backgroundColor = .blue
}
#elseif os(macOS)
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { view in
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { view in
view.layer?.backgroundColor = NSColor.blue.cgColor
}
#endif
Expand All @@ -353,7 +353,7 @@ struct GenericViewShowcase: View {
view.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { view in
.introspect(.view, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { view in
view.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand All @@ -380,7 +380,7 @@ struct SimpleElementsShowcase: View {
textField.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.textField, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { textField in
.introspect(.textField, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { textField in
textField.backgroundColor = .red
}
#endif
Expand All @@ -395,7 +395,7 @@ struct SimpleElementsShowcase: View {
textField.backgroundColor = .green
}
#elseif os(macOS)
.introspect(.textField, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { textField in
.introspect(.textField, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { textField in
textField.backgroundColor = .green
}
#endif
Expand All @@ -413,7 +413,7 @@ struct SimpleElementsShowcase: View {
toggle.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.toggle, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { toggle in
.introspect(.toggle, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { toggle in
toggle.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand All @@ -427,7 +427,7 @@ struct SimpleElementsShowcase: View {
toggle.backgroundColor = .green
}
#elseif os(macOS)
.introspect(.toggle, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { toggle in
.introspect(.toggle, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { toggle in
toggle.layer?.backgroundColor = NSColor.green.cgColor
}
#endif
Expand All @@ -440,7 +440,7 @@ struct SimpleElementsShowcase: View {
slider.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.slider, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { slider in
.introspect(.slider, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { slider in
slider.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand All @@ -451,7 +451,7 @@ struct SimpleElementsShowcase: View {
slider.backgroundColor = .green
}
#elseif os(macOS)
.introspect(.slider, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { slider in
.introspect(.slider, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { slider in
slider.layer?.backgroundColor = NSColor.green.cgColor
}
#endif
Expand All @@ -466,7 +466,7 @@ struct SimpleElementsShowcase: View {
stepper.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.stepper, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { stepper in
.introspect(.stepper, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { stepper in
stepper.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand All @@ -479,7 +479,7 @@ struct SimpleElementsShowcase: View {
stepper.backgroundColor = .green
}
#elseif os(macOS)
.introspect(.stepper, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { stepper in
.introspect(.stepper, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { stepper in
stepper.layer?.backgroundColor = NSColor.green.cgColor
}
#endif
Expand All @@ -495,7 +495,7 @@ struct SimpleElementsShowcase: View {
datePicker.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { datePicker in
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { datePicker in
datePicker.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand All @@ -517,7 +517,7 @@ struct SimpleElementsShowcase: View {
datePicker.backgroundColor = .red
}
#elseif os(macOS)
.introspect(.picker(style: .segmented), on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) { datePicker in
.introspect(.picker(style: .segmented), on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) { datePicker in
datePicker.layer?.backgroundColor = NSColor.red.cgColor
}
#endif
Expand Down
4 changes: 2 additions & 2 deletions Sources/ViewTypes/DatePickerWithCompactStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ extension IntrospectableViewType where Self == DatePickerWithCompactStyleType {
#if canImport(UIKit)
extension iOSViewVersion<DatePickerWithCompactStyleType, UIDatePicker> {
@available(*, unavailable, message: ".datePickerStyle(.compact) isn't available on iOS 13")
public static let v13 = Self(for: .v13)
public static let v13 = Self.unavailable()
public static let v14 = Self(for: .v14)
public static let v15 = Self(for: .v15)
public static let v16 = Self(for: .v16)
Expand All @@ -83,7 +83,7 @@ extension visionOSViewVersion<DatePickerWithCompactStyleType, UIDatePicker> {
#elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
extension macOSViewVersion<DatePickerWithCompactStyleType, NSDatePicker> {
@available(*, unavailable, message: ".datePickerStyle(.compact) isn't available on macOS 10.15")
public static let v10_15 = Self(for: .v10_15)
public static let v10_15 = Self.unavailable()
public static let v10_15_4 = Self(for: .v10_15_4)
public static let v11 = Self(for: .v11)
public static let v12 = Self(for: .v12)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ViewTypes/SignInWithAppleButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import SwiftUI
/// } onCompletion: { result in
/// // do something with result
/// }
/// .introspect(.signInWithAppleButton, on: .macOS(.v11, .v12, .v13, .v14),) {
/// .introspect(.signInWithAppleButton, on: .macOS(.v11, .v12, .v13, .v14, .v15),) {
/// print(type(of: $0)) // ASAuthorizationAppleIDButton
/// }
/// }
Expand Down
5 changes: 3 additions & 2 deletions Sources/ViewTypes/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import SwiftUI
/// Text("Tab 1").tabItem { Text("Tab 1") }
/// Text("Tab 2").tabItem { Text("Tab 2") }
/// }
/// .introspect(.tabView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15)) {
/// .introspect(.tabView, on: .macOS(.v10_15, .v11, .v12, .v13, .v14)) {
/// print(type(of: $0)) // NSTabView
/// }
/// }
Expand Down Expand Up @@ -96,7 +96,8 @@ extension macOSViewVersion<TabViewType, NSTabView> {
public static let v12 = Self(for: .v12)
public static let v13 = Self(for: .v13)
public static let v14 = Self(for: .v14)
public static let v15 = Self(for: .v15)
@available(*, unavailable, message: "TabView is no longer backed by NSTabView starting macOS 15")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true. I successfully patch TabView's underlying NSTabView on macOS 15.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for letting me know. I'll look into it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thcyron hmm doesn't seem to work for me. Do you have a repro I could use? I'm running the latest macOS (15.6). The closest thing to a NSTabView the view graph inspector shows is a NSToolBarView but that's a private class of no real usefulness to anyone outside Apple.

Screenshot 2025-07-30 at 21 18 31

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davdroman Wrap TabView in a GroupBox, then it's a NSTabView.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that does work. The GroupBox thing is a significant caveat, so I think in order to avoid confusion I'll create a separate type for it altogether... something like .tabView(placement: .groupBox) and keep the unavailable message in .tabView to let users know about this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To your knowledge, is there any other container view capable of this transformation other than GroupBox?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimal example extracted from my app. Here, the ZStack seems to be key to force the TabView to not be part of the toolbar.

struct MainView: View {
    @ViewBuilder
    private var tabView: some View {
        TabView {
            Text("A")
            Text("B")
        }
        .introspect(.tabView, on: .macOS(.v15)) {
            $0.tabViewType = .noTabsNoBorder
        }
    }

    var body: some View {
        NavigationSplitView {
            Text("Sidebar")
        } detail: {
            ZStack {
                tabView
            }
        }
    }
}

Copy link
Collaborator Author

@davdroman davdroman Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks for this. I'm torn between being specific:

.tabView(placement: .groupBox)
.tabView(placement: .zStack)
// ... anything else?

or being general:

.tabView(placement: .nonRoot) // is there a better name? .container perhaps? or .detached?

As SwiftUI tends to add more and more views each year it might be a good shout to be a bit more general to cover all possible cases without having to do heavy maintenance...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the ZStack is crucial in the example I gave to get the traditional NSTabView layout, just wrapping in a ZStack isn't enough in all cases. If you drop the split view but keep the ZStack, you get the toolbar tab view layout again without a NSTabView.

var body: some View {
    ZStack {
        tabView
            .introspect(.tabView, on: .macOS(.v15)) {
                // never called
                $0.tabViewType = .noTabsNoBorder
            }
    }
}

As you said, we will never fully understand when a TabView is a NSTabView and when it isn't. Thus, I would suggest to keep the pre-1.4.0 behavior: allow .introspect(.tabView, on: .macOS(.v15)) and make it best-effort. If there is a NSTabView, you get it, but there isn't, you don't.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. PR is up now btw #477

public static let v15 = Self.unavailable()
}
#endif
#endif
Expand Down
8 changes: 4 additions & 4 deletions Tests/Tests/ViewTypes/ButtonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,24 @@ final class ButtonTests: XCTestCase {
Button("Button 0", action: {})
.buttonStyle(.bordered)
#if os(macOS)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy0)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy0)
#endif

Button("Button 1", action: {})
.buttonStyle(.borderless)
#if os(macOS)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy1)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy1)
#endif

Button("Button 2", action: {})
.buttonStyle(.link)
#if os(macOS)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy2)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy2)
#endif

Button("Button 3", action: {})
#if os(macOS)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy3)
.introspect(.button, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy3)
#endif
}
} extraAssertions: {
Expand Down
6 changes: 3 additions & 3 deletions Tests/Tests/ViewTypes/ColorPickerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ final class ColorPickerTests: XCTestCase {
#if os(iOS) || os(visionOS)
.introspect(.colorPicker, on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy0)
#elseif os(macOS)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14), customize: spy0)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14, .v15), customize: spy0)
#endif

ColorPicker("", selection: .constant(PlatformColor.green.cgColor))
#if os(iOS) || os(visionOS)
.introspect(.colorPicker, on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy1)
#elseif os(macOS)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14), customize: spy1)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14, .v15), customize: spy1)
#endif

ColorPicker("", selection: .constant(PlatformColor.blue.cgColor))
#if os(iOS) || os(visionOS)
.introspect(.colorPicker, on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy2)
#elseif os(macOS)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14), customize: spy2)
.introspect(.colorPicker, on: .macOS(.v11, .v12, .v13, .v14, .v15), customize: spy2)
#endif
}
} extraAssertions: {
Expand Down
6 changes: 3 additions & 3 deletions Tests/Tests/ViewTypes/DatePickerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,23 @@ final class DatePickerTests: XCTestCase {
#if os(iOS) || os(visionOS)
.introspect(.datePicker, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy0)
#elseif os(macOS)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy0)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy0)
#endif
.cornerRadius(8)

DatePicker("", selection: .constant(date1))
#if os(iOS) || os(visionOS)
.introspect(.datePicker, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy1)
#elseif os(macOS)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy1)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy1)
#endif
.cornerRadius(8)

DatePicker("", selection: .constant(date2))
#if os(iOS) || os(visionOS)
.introspect(.datePicker, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy2)
#elseif os(macOS)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14), customize: spy2)
.introspect(.datePicker, on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15), customize: spy2)
#endif
}
} extraAssertions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class DatePickerWithCompactStyleTests: XCTestCase {
#if os(iOS) || os(visionOS)
.introspect(.datePicker(style: .compact), on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy0)
#elseif os(macOS)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14), customize: spy0)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14, .v15), customize: spy0)
#endif
.cornerRadius(8)

Expand All @@ -41,7 +41,7 @@ final class DatePickerWithCompactStyleTests: XCTestCase {
#if os(iOS) || os(visionOS)
.introspect(.datePicker(style: .compact), on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy1)
#elseif os(macOS)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14), customize: spy1)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14, .v15), customize: spy1)
#endif
.cornerRadius(8)

Expand All @@ -50,7 +50,7 @@ final class DatePickerWithCompactStyleTests: XCTestCase {
#if os(iOS) || os(visionOS)
.introspect(.datePicker(style: .compact), on: .iOS(.v14, .v15, .v16, .v17, .v18), .visionOS(.v1, .v2), customize: spy2)
#elseif os(macOS)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14), customize: spy2)
.introspect(.datePicker(style: .compact), on: .macOS(.v10_15_4, .v11, .v12, .v13, .v14, .v15), customize: spy2)
#endif
}
} extraAssertions: {
Expand Down
Loading