Skip to content

Commit 2407a98

Browse files
committed
Introduce 'inspect' modifiers for accessing underlying native widgets
These changes make platform-specific native customizations significantly easier to perform. Hopefully this will make SwiftCrossUI significantly more viable for actual production apps that often just need to get things working even if there's not a nice neat first party API for it yet. Inspiration was taken from swiftui-introspect.
1 parent fd6a9a4 commit 2407a98

File tree

16 files changed

+1014
-17
lines changed

16 files changed

+1014
-17
lines changed

.github/workflows/build-test-and-docs.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
cd Examples && \
4242
swift build --target GtkBackend && \
4343
swift build --target Gtk3Backend && \
44+
swift build --target GtkExample && \
4445
swift build --target CounterExample && \
4546
swift build --target ControlsExample && \
4647
swift build --target RandomNumberGeneratorExample && \
@@ -51,8 +52,9 @@ jobs:
5152
swift build --target StressTestExample && \
5253
swift build --target SpreadsheetExample && \
5354
swift build --target NotesExample && \
54-
swift build --target GtkExample && \
55-
swift build --target PathsExample
55+
swift build --target PathsExample && \
56+
swift build --target WebViewExample && \
57+
swift build --target AdvancedCustomizationExample
5658
5759
- name: Test
5860
run: swift test --test-product swift-cross-uiPackageTests
@@ -101,6 +103,8 @@ jobs:
101103
buildtarget StressTestExample
102104
buildtarget NotesExample
103105
buildtarget PathsExample
106+
buildtarget WebViewExample
107+
buildtarget AdvancedCustomizationExample
104108
105109
if [ $device_type != TV ]; then
106110
# Slider is not implemented for tvOS
@@ -161,6 +165,8 @@ jobs:
161165
buildtarget PathsExample
162166
buildtarget ControlsExample
163167
buildtarget RandomNumberGeneratorExample
168+
buildtarget WebViewExample
169+
buildtarget AdvancedCustomizationExample
164170
# TODO test whether this works on Catalyst
165171
# buildtarget SplitExample
166172
@@ -281,6 +287,7 @@ jobs:
281287
- name: Build examples
282288
working-directory: ./Examples
283289
run: |
290+
swift build --target GtkExample && \
284291
swift build --target CounterExample && \
285292
swift build --target ControlsExample && \
286293
swift build --target RandomNumberGeneratorExample && \
@@ -291,7 +298,8 @@ jobs:
291298
swift build --target StressTestExample && \
292299
swift build --target SpreadsheetExample && \
293300
swift build --target NotesExample && \
294-
swift build --target GtkExample
301+
swift build --target PathsExample && \
302+
swift build --target AdvancedCustomizationExample
295303
296304
- name: Test
297305
run: swift test --test-product swift-cross-uiPackageTests

Examples/Bundler.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ version = '0.1.0'
5959
identifier = 'dev.swiftcrossui.WebViewExample'
6060
product = 'WebViewExample'
6161
version = '0.1.0'
62+
63+
[apps.AdvancedCustomizationExample]
64+
identifier = 'dev.swiftcrossui.AdvancedCustomizationExample'
65+
product = 'AdvancedCustomizationExample'
66+
version = '0.1.0'

Examples/Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 5.10
22

33
import Foundation
44
import PackageDescription
@@ -72,6 +72,11 @@ let package = Package(
7272
.executableTarget(
7373
name: "WebViewExample",
7474
dependencies: exampleDependencies
75+
),
76+
.executableTarget(
77+
name: "AdvancedCustomizationExample",
78+
dependencies: exampleDependencies,
79+
resources: [.copy("Banner.png")]
7580
)
7681
]
7782
)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import DefaultBackend
2+
import Foundation
3+
import SwiftCrossUI
4+
5+
#if canImport(WinUIBackend)
6+
import WinUI
7+
#endif
8+
9+
#if canImport(SwiftBundlerRuntime)
10+
import SwiftBundlerRuntime
11+
#endif
12+
13+
@main
14+
@HotReloadable
15+
struct CounterApp: App {
16+
@State var count = 0
17+
@State var value = 0.0
18+
@State var color: String? = nil
19+
@State var name = ""
20+
21+
var body: some Scene {
22+
WindowGroup("CounterExample: \(count)") {
23+
#hotReloadable {
24+
ScrollView {
25+
HStack(spacing: 20) {
26+
Button("-") {
27+
count -= 1
28+
}
29+
30+
Text("Count: \(count)")
31+
.inspect { text in
32+
#if canImport(AppKitBackend)
33+
text.isSelectable = true
34+
#elseif canImport(UIKitBackend)
35+
#if targetEnvironment(macCatalyst)
36+
text.isHighlighted = true
37+
text.highlightTextColor = .yellow
38+
#endif
39+
#elseif canImport(WinUIBackend)
40+
text.isTextSelectionEnabled = true
41+
#elseif canImport(GtkBackend)
42+
text.selectable = true
43+
#elseif canImport(Gtk3Backend)
44+
text.selectable = true
45+
#endif
46+
}
47+
48+
Button("+") {
49+
count += 1
50+
}.inspect(.afterUpdate) { button in
51+
#if canImport(AppKitBackend)
52+
// Button is an NSButton on macOS
53+
button.bezelColor = .red
54+
#elseif canImport(UIKitBackend)
55+
if #available(iOS 15.0, *) {
56+
button.configuration = .bordered()
57+
}
58+
#elseif canImport(WinUIBackend)
59+
button.cornerRadius.topLeft = 10
60+
let brush = WinUI.SolidColorBrush()
61+
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
62+
button.background = brush
63+
#elseif canImport(GtkBackend)
64+
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
65+
#elseif canImport(Gtk3Backend)
66+
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
67+
#endif
68+
}
69+
}
70+
71+
Slider($value, minimum: 0, maximum: 10)
72+
.inspect { slider in
73+
#if canImport(AppKitBackend)
74+
slider.numberOfTickMarks = 10
75+
#elseif canImport(UIKitBackend)
76+
slider.thumbTintColor = .blue
77+
#elseif canImport(WinUIBackend)
78+
slider.isThumbToolTipEnabled = true
79+
#elseif canImport(GtkBackend)
80+
slider.drawValue = true
81+
#elseif canImport(Gtk3Backend)
82+
slider.drawValue = true
83+
#endif
84+
}
85+
86+
#if !canImport(Gtk3Backend)
87+
Picker(of: ["Red", "Green", "Blue"], selection: $color)
88+
.inspect(.afterUpdate) { picker in
89+
#if canImport(AppKitBackend)
90+
picker.preferredEdge = .maxX
91+
#elseif canImport(UIKitBackend) && os(iOS)
92+
// Can't think of something to do to the
93+
// UIPickerView, but the point is that you
94+
// could do something if you needed to!
95+
// This would be a UITableView on tvOS.
96+
// And could be either a UITableView or a
97+
// UIPickerView on Mac Catalyst depending
98+
// on Mac Catalyst version and interface
99+
// idiom.
100+
#elseif canImport(WinUIBackend)
101+
let brush = WinUI.SolidColorBrush()
102+
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
103+
picker.background = brush
104+
#elseif canImport(GtkBackend)
105+
picker.enableSearch = true
106+
#endif
107+
}
108+
#endif
109+
110+
TextField("Name", text: $name)
111+
.inspect(.afterUpdate) { textField in
112+
#if canImport(AppKitBackend)
113+
textField.backgroundColor = .blue
114+
#elseif canImport(UIKitBackend)
115+
textField.borderStyle = .bezel
116+
#elseif canImport(WinUIBackend)
117+
textField.selectionHighlightColor.color = .init(a: 255, r: 0, g: 255, b: 0)
118+
let brush = WinUI.SolidColorBrush()
119+
brush.color = .init(a: 255, r: 0, g: 0, b: 255)
120+
textField.background = brush
121+
#elseif canImport(GtkBackend)
122+
textField.xalign = 1
123+
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
124+
#elseif canImport(Gtk3Backend)
125+
textField.hasFrame = false
126+
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
127+
#endif
128+
}
129+
130+
ScrollView {
131+
ForEach(Array(1...50)) { number in
132+
Text("Line \(number)")
133+
}.padding()
134+
}.inspect(.afterUpdate) { scrollView in
135+
#if canImport(AppKitBackend)
136+
scrollView.borderType = .grooveBorder
137+
#elseif canImport(UIKitBackend)
138+
scrollView.alwaysBounceHorizontal = true
139+
#elseif canImport(WinUIBackend)
140+
let brush = WinUI.SolidColorBrush()
141+
brush.color = .init(a: 255, r: 0, g: 255, b: 0)
142+
scrollView.borderBrush = brush
143+
scrollView.borderThickness = .init(
144+
left: 1, top: 1, right: 1, bottom: 1
145+
)
146+
#elseif canImport(GtkBackend)
147+
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
148+
#elseif canImport(Gtk3Backend)
149+
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
150+
#endif
151+
}.frame(height: 200)
152+
153+
List(["Red", "Green", "Blue"], id: \.self, selection: $color) { color in
154+
Text(color)
155+
}.inspect(.afterUpdate) { table in
156+
#if canImport(AppKitBackend)
157+
table.usesAlternatingRowBackgroundColors = true
158+
#elseif canImport(UIKitBackend)
159+
table.isEditing = true
160+
#elseif canImport(WinUIBackend)
161+
let brush = WinUI.SolidColorBrush()
162+
brush.color = .init(a: 255, r: 255, g: 0, b: 255)
163+
table.borderBrush = brush
164+
table.borderThickness = .init(
165+
left: 1, top: 1, right: 1, bottom: 1
166+
)
167+
#elseif canImport(GtkBackend)
168+
table.showSeparators = true
169+
#elseif canImport(Gtk3Backend)
170+
table.selectionMode = .multiple
171+
#endif
172+
}
173+
174+
Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
175+
.resizable()
176+
.inspect(.afterUpdate) { image in
177+
#if canImport(AppKitBackend)
178+
image.isEditable = true
179+
#elseif canImport(UIKitBackend)
180+
image.layer.borderWidth = 1
181+
image.layer.borderColor = .init(red: 0, green: 1, blue: 0, alpha: 1)
182+
#elseif canImport(WinUIBackend)
183+
// Couldn't find anything visually interesting
184+
// to do to the WinUI.Image, but the point is
185+
// that you could do something if you wanted to.
186+
#elseif canImport(GtkBackend)
187+
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
188+
#elseif canImport(Gtk3Backend)
189+
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
190+
#endif
191+
}
192+
.aspectRatio(contentMode: .fit)
193+
}.padding()
194+
}
195+
}
196+
.defaultSize(width: 400, height: 200)
197+
}
198+
}
Loading
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import AppKit
2+
import SwiftCrossUI
3+
4+
extension View {
5+
public func inspect(
6+
_ inspectionPoints: InspectionPoints = .onCreate,
7+
_ action: @escaping @MainActor @Sendable (NSView) -> Void
8+
) -> some View {
9+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
10+
}
11+
}
12+
13+
extension Button {
14+
public func inspect(
15+
_ inspectionPoints: InspectionPoints = .onCreate,
16+
_ action: @escaping @MainActor @Sendable (NSButton) -> Void
17+
) -> some View {
18+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
19+
}
20+
}
21+
22+
extension Text {
23+
public func inspect(
24+
_ inspectionPoints: InspectionPoints = .onCreate,
25+
_ action: @escaping @MainActor @Sendable (NSTextField) -> Void
26+
) -> some View {
27+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
28+
}
29+
}
30+
31+
extension Slider {
32+
public func inspect(
33+
_ inspectionPoints: InspectionPoints = .onCreate,
34+
_ action: @escaping @MainActor @Sendable (NSSlider) -> Void
35+
) -> some View {
36+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
37+
}
38+
}
39+
40+
extension Picker {
41+
public func inspect(
42+
_ inspectionPoints: InspectionPoints = .onCreate,
43+
_ action: @escaping @MainActor @Sendable (NSPopUpButton) -> Void
44+
) -> some View {
45+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
46+
}
47+
}
48+
49+
extension TextField {
50+
public func inspect(
51+
_ inspectionPoints: InspectionPoints = .onCreate,
52+
_ action: @escaping @MainActor @Sendable (NSTextField) -> Void
53+
) -> some View {
54+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
55+
}
56+
}
57+
58+
extension ScrollView {
59+
public func inspect(
60+
_ inspectionPoints: InspectionPoints = .onCreate,
61+
_ action: @escaping @MainActor @Sendable (NSScrollView) -> Void
62+
) -> some View {
63+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
64+
}
65+
}
66+
67+
extension List {
68+
public func inspect(
69+
_ inspectionPoints: InspectionPoints = .onCreate,
70+
_ action: @escaping @MainActor @Sendable (NSTableView) -> Void
71+
) -> some View {
72+
InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSScrollView) in
73+
action(view.documentView as! NSTableView)
74+
}
75+
}
76+
}
77+
78+
extension NavigationSplitView {
79+
public func inspect(
80+
_ inspectionPoints: InspectionPoints = .onCreate,
81+
_ action: @escaping @MainActor @Sendable (NSSplitView) -> Void
82+
) -> some View {
83+
InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSView) in
84+
action(view.subviews[0] as! NSSplitView)
85+
}
86+
}
87+
}
88+
89+
extension Image {
90+
public func inspect(
91+
_ inspectionPoints: InspectionPoints = .onCreate,
92+
_ action: @escaping @MainActor @Sendable (NSImageView) -> Void
93+
) -> some View {
94+
InspectView(child: self, inspectionPoints: inspectionPoints) { (_: NSView, children: ImageChildren) in
95+
action(children.imageWidget.into())
96+
}
97+
}
98+
}
99+
100+
extension Table {
101+
public func inspect(
102+
_ inspectionPoints: InspectionPoints = .onCreate,
103+
_ action: @escaping @MainActor @Sendable (NSScrollView) -> Void
104+
) -> some View {
105+
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
106+
}
107+
}

0 commit comments

Comments
 (0)