Skip to content

Commit 650ed52

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 650ed52

File tree

16 files changed

+1012
-17
lines changed

16 files changed

+1012
-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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
text.isHighlighted = true
36+
text.highlightTextColor = .yellow
37+
#elseif canImport(WinUIBackend)
38+
text.isTextSelectionEnabled = true
39+
#elseif canImport(GtkBackend)
40+
text.selectable = true
41+
#elseif canImport(Gtk3Backend)
42+
text.selectable = true
43+
#endif
44+
}
45+
46+
Button("+") {
47+
count += 1
48+
}.inspect(.afterUpdate) { button in
49+
#if canImport(AppKitBackend)
50+
// Button is an NSButton on macOS
51+
button.bezelColor = .red
52+
#elseif canImport(UIKitBackend)
53+
if #available(iOS 15.0, *) {
54+
button.configuration = .bordered()
55+
}
56+
#elseif canImport(WinUIBackend)
57+
button.cornerRadius.topLeft = 10
58+
let brush = WinUI.SolidColorBrush()
59+
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
60+
button.background = brush
61+
#elseif canImport(GtkBackend)
62+
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
63+
#elseif canImport(Gtk3Backend)
64+
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
65+
#endif
66+
}
67+
}
68+
69+
Slider($value, minimum: 0, maximum: 10)
70+
.inspect { slider in
71+
#if canImport(AppKitBackend)
72+
slider.numberOfTickMarks = 10
73+
#elseif canImport(UIKitBackend)
74+
slider.thumbTintColor = .blue
75+
#elseif canImport(WinUIBackend)
76+
slider.isThumbToolTipEnabled = true
77+
#elseif canImport(GtkBackend)
78+
slider.drawValue = true
79+
#elseif canImport(Gtk3Backend)
80+
slider.drawValue = true
81+
#endif
82+
}
83+
84+
#if !canImport(Gtk3Backend)
85+
Picker(of: ["Red", "Green", "Blue"], selection: $color)
86+
.inspect(.afterUpdate) { picker in
87+
#if canImport(AppKitBackend)
88+
picker.preferredEdge = .maxX
89+
#elseif canImport(UIKitBackend) && os(iOS)
90+
// Can't think of something to do to the
91+
// UIPickerView, but the point is that you
92+
// could do something if you needed to!
93+
// This would be a UITableView on tvOS.
94+
// And could be either a UITableView or a
95+
// UIPickerView on Mac Catalyst depending
96+
// on Mac Catalyst version and interface
97+
// idiom.
98+
#elseif canImport(WinUIBackend)
99+
let brush = WinUI.SolidColorBrush()
100+
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
101+
picker.background = brush
102+
#elseif canImport(GtkBackend)
103+
picker.enableSearch = true
104+
#endif
105+
}
106+
#endif
107+
108+
TextField("Name", text: $name)
109+
.inspect(.afterUpdate) { textField in
110+
#if canImport(AppKitBackend)
111+
textField.backgroundColor = .blue
112+
#elseif canImport(UIKitBackend)
113+
textField.borderStyle = .bezel
114+
#elseif canImport(WinUIBackend)
115+
textField.selectionHighlightColor.color = .init(a: 255, r: 0, g: 255, b: 0)
116+
let brush = WinUI.SolidColorBrush()
117+
brush.color = .init(a: 255, r: 0, g: 0, b: 255)
118+
textField.background = brush
119+
#elseif canImport(GtkBackend)
120+
textField.xalign = 1
121+
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
122+
#elseif canImport(Gtk3Backend)
123+
textField.hasFrame = false
124+
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
125+
#endif
126+
}
127+
128+
ScrollView {
129+
ForEach(Array(1...50)) { number in
130+
Text("Line \(number)")
131+
}.padding()
132+
}.inspect(.afterUpdate) { scrollView in
133+
#if canImport(AppKitBackend)
134+
scrollView.borderType = .grooveBorder
135+
#elseif canImport(UIKitBackend)
136+
scrollView.alwaysBounceHorizontal = true
137+
#elseif canImport(WinUIBackend)
138+
let brush = WinUI.SolidColorBrush()
139+
brush.color = .init(a: 255, r: 0, g: 255, b: 0)
140+
scrollView.borderBrush = brush
141+
scrollView.borderThickness = .init(
142+
left: 1, top: 1, right: 1, bottom: 1
143+
)
144+
#elseif canImport(GtkBackend)
145+
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
146+
#elseif canImport(Gtk3Backend)
147+
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
148+
#endif
149+
}.frame(height: 200)
150+
151+
List(["Red", "Green", "Blue"], id: \.self, selection: $color) { color in
152+
Text(color)
153+
}.inspect(.afterUpdate) { table in
154+
#if canImport(AppKitBackend)
155+
table.usesAlternatingRowBackgroundColors = true
156+
#elseif canImport(UIKitBackend)
157+
table.isEditing = true
158+
#elseif canImport(WinUIBackend)
159+
let brush = WinUI.SolidColorBrush()
160+
brush.color = .init(a: 255, r: 255, g: 0, b: 255)
161+
table.borderBrush = brush
162+
table.borderThickness = .init(
163+
left: 1, top: 1, right: 1, bottom: 1
164+
)
165+
#elseif canImport(GtkBackend)
166+
table.showSeparators = true
167+
#elseif canImport(Gtk3Backend)
168+
table.selectionMode = .multiple
169+
#endif
170+
}
171+
172+
Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
173+
.resizable()
174+
.inspect(.afterUpdate) { image in
175+
#if canImport(AppKitBackend)
176+
image.isEditable = true
177+
#elseif canImport(UIKitBackend)
178+
image.layer.borderWidth = 1
179+
image.layer.borderColor = .init(red: 0, green: 1, blue: 0, alpha: 1)
180+
#elseif canImport(WinUIBackend)
181+
// Couldn't find anything visually interesting
182+
// to do to the WinUI.Image, but the point is
183+
// that you could do something if you wanted to.
184+
#elseif canImport(GtkBackend)
185+
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
186+
#elseif canImport(Gtk3Backend)
187+
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
188+
#endif
189+
}
190+
.aspectRatio(contentMode: .fit)
191+
}.padding()
192+
}
193+
}
194+
.defaultSize(width: 400, height: 200)
195+
}
196+
}
14.6 KB
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)