Skip to content

Improve interoperability with native UI frameworks #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
14 changes: 11 additions & 3 deletions .github/workflows/build-test-and-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
cd Examples && \
swift build --target GtkBackend && \
swift build --target Gtk3Backend && \
swift build --target GtkExample && \
swift build --target CounterExample && \
swift build --target ControlsExample && \
swift build --target RandomNumberGeneratorExample && \
Expand All @@ -51,8 +52,9 @@ jobs:
swift build --target StressTestExample && \
swift build --target SpreadsheetExample && \
swift build --target NotesExample && \
swift build --target GtkExample && \
swift build --target PathsExample
swift build --target PathsExample && \
swift build --target WebViewExample && \
swift build --target AdvancedCustomizationExample

- name: Test
run: swift test --test-product swift-cross-uiPackageTests
Expand Down Expand Up @@ -101,6 +103,8 @@ jobs:
buildtarget StressTestExample
buildtarget NotesExample
buildtarget PathsExample
buildtarget WebViewExample
buildtarget AdvancedCustomizationExample

if [ $device_type != TV ]; then
# Slider is not implemented for tvOS
Expand Down Expand Up @@ -161,6 +165,8 @@ jobs:
buildtarget PathsExample
buildtarget ControlsExample
buildtarget RandomNumberGeneratorExample
buildtarget WebViewExample
buildtarget AdvancedCustomizationExample
# TODO test whether this works on Catalyst
# buildtarget SplitExample

Expand Down Expand Up @@ -281,6 +287,7 @@ jobs:
- name: Build examples
working-directory: ./Examples
run: |
swift build --target GtkExample && \
swift build --target CounterExample && \
swift build --target ControlsExample && \
swift build --target RandomNumberGeneratorExample && \
Expand All @@ -291,7 +298,8 @@ jobs:
swift build --target StressTestExample && \
swift build --target SpreadsheetExample && \
swift build --target NotesExample && \
swift build --target GtkExample
swift build --target PathsExample && \
swift build --target AdvancedCustomizationExample

- name: Test
run: swift test --test-product swift-cross-uiPackageTests
Expand Down
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ version = '0.1.0'
identifier = 'dev.swiftcrossui.WebViewExample'
product = 'WebViewExample'
version = '0.1.0'

[apps.AdvancedCustomizationExample]
identifier = 'dev.swiftcrossui.AdvancedCustomizationExample'
product = 'AdvancedCustomizationExample'
version = '0.1.0'
7 changes: 6 additions & 1 deletion Examples/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.10

import Foundation
import PackageDescription
Expand Down Expand Up @@ -72,6 +72,11 @@ let package = Package(
.executableTarget(
name: "WebViewExample",
dependencies: exampleDependencies
),
.executableTarget(
name: "AdvancedCustomizationExample",
dependencies: exampleDependencies,
resources: [.copy("Banner.png")]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import DefaultBackend
import Foundation
import SwiftCrossUI

#if canImport(WinUIBackend)
import WinUI
#endif

#if canImport(SwiftBundlerRuntime)
import SwiftBundlerRuntime
#endif

@main
@HotReloadable
struct CounterApp: App {
@State var count = 0
@State var value = 0.0
@State var color: String? = nil
@State var name = ""

var body: some Scene {
WindowGroup("CounterExample: \(count)") {
#hotReloadable {
ScrollView {
HStack(spacing: 20) {
Button("-") {
count -= 1
}

Text("Count: \(count)")
.inspect { text in
#if canImport(AppKitBackend)
text.isSelectable = true
#elseif canImport(UIKitBackend)
#if !targetEnvironment(macCatalyst)
text.isHighlighted = true

Check failure on line 36 in Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPad)

value of type 'UIView' has no member 'isHighlighted'

Check failure on line 36 in Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPad)

value of type 'UIView' has no member 'isHighlighted'

Check failure on line 36 in Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

value of type 'UIView' has no member 'isHighlighted'

Check failure on line 36 in Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

value of type 'UIView' has no member 'isHighlighted'
text.highlightTextColor = .yellow
#endif
#elseif canImport(WinUIBackend)
text.isTextSelectionEnabled = true
#elseif canImport(GtkBackend)
text.selectable = true
#elseif canImport(Gtk3Backend)
text.selectable = true
#endif
}

Button("+") {
count += 1
}.inspect(.afterUpdate) { button in
#if canImport(AppKitBackend)
// Button is an NSButton on macOS
button.bezelColor = .red
#elseif canImport(UIKitBackend)
if #available(iOS 15.0, *) {
button.configuration = .bordered()
}
#elseif canImport(WinUIBackend)
button.cornerRadius.topLeft = 10
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
button.background = brush
#elseif canImport(GtkBackend)
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
#elseif canImport(Gtk3Backend)
button.css.set(property: .backgroundColor(.init(1, 0, 0)))
#endif
}
}

Slider($value, minimum: 0, maximum: 10)
.inspect { slider in
#if canImport(AppKitBackend)
slider.numberOfTickMarks = 10
#elseif canImport(UIKitBackend)
slider.thumbTintColor = .blue
#elseif canImport(WinUIBackend)
slider.isThumbToolTipEnabled = true
#elseif canImport(GtkBackend)
slider.drawValue = true
#elseif canImport(Gtk3Backend)
slider.drawValue = true
#endif
}

#if !canImport(Gtk3Backend)
Picker(of: ["Red", "Green", "Blue"], selection: $color)
.inspect(.afterUpdate) { picker in
#if canImport(AppKitBackend)
picker.preferredEdge = .maxX
#elseif canImport(UIKitBackend) && os(iOS)
// Can't think of something to do to the
// UIPickerView, but the point is that you
// could do something if you needed to!
// This would be a UITableView on tvOS.
// And could be either a UITableView or a
// UIPickerView on Mac Catalyst depending
// on Mac Catalyst version and interface
// idiom.
#elseif canImport(WinUIBackend)
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 255, g: 0, b: 0)
picker.background = brush
#elseif canImport(GtkBackend)
picker.enableSearch = true
#endif
}
#endif

TextField("Name", text: $name)
.inspect(.afterUpdate) { textField in
#if canImport(AppKitBackend)
textField.backgroundColor = .blue
#elseif canImport(UIKitBackend)
textField.borderStyle = .bezel
#elseif canImport(WinUIBackend)
textField.selectionHighlightColor.color = .init(a: 255, r: 0, g: 255, b: 0)
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 0, g: 0, b: 255)
textField.background = brush
#elseif canImport(GtkBackend)
textField.xalign = 1
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
#elseif canImport(Gtk3Backend)
textField.hasFrame = false
textField.css.set(property: .backgroundColor(.init(0, 0, 1)))
#endif
}

ScrollView {
ForEach(Array(1...50)) { number in
Text("Line \(number)")
}.padding()
}.inspect(.afterUpdate) { scrollView in
#if canImport(AppKitBackend)
scrollView.borderType = .grooveBorder
#elseif canImport(UIKitBackend)
scrollView.alwaysBounceHorizontal = true
#elseif canImport(WinUIBackend)
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 0, g: 255, b: 0)
scrollView.borderBrush = brush
scrollView.borderThickness = .init(
left: 1, top: 1, right: 1, bottom: 1
)
#elseif canImport(GtkBackend)
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
#elseif canImport(Gtk3Backend)
scrollView.css.set(property: .border(color: .init(1, 0, 0), width: 2))
#endif
}.frame(height: 200)

List(["Red", "Green", "Blue"], id: \.self, selection: $color) { color in
Text(color)
}.inspect(.afterUpdate) { table in
#if canImport(AppKitBackend)
table.usesAlternatingRowBackgroundColors = true
#elseif canImport(UIKitBackend)
table.isEditing = true
#elseif canImport(WinUIBackend)
let brush = WinUI.SolidColorBrush()
brush.color = .init(a: 255, r: 255, g: 0, b: 255)
table.borderBrush = brush
table.borderThickness = .init(
left: 1, top: 1, right: 1, bottom: 1
)
#elseif canImport(GtkBackend)
table.showSeparators = true
#elseif canImport(Gtk3Backend)
table.selectionMode = .multiple
#endif
}

Image(Bundle.module.bundleURL.appendingPathComponent("Banner.png"))
.resizable()
.inspect(.afterUpdate) { image in
#if canImport(AppKitBackend)
image.isEditable = true
#elseif canImport(UIKitBackend)
image.layer.borderWidth = 1
image.layer.borderColor = .init(red: 0, green: 1, blue: 0, alpha: 1)
#elseif canImport(WinUIBackend)
// Couldn't find anything visually interesting
// to do to the WinUI.Image, but the point is
// that you could do something if you wanted to.
#elseif canImport(GtkBackend)
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
#elseif canImport(Gtk3Backend)
image.css.set(property: .border(color: .init(0, 1, 0), width: 2))
#endif
}
.aspectRatio(contentMode: .fit)
}.padding()
}
}
.defaultSize(width: 400, height: 200)
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions Sources/AppKitBackend/InspectionModifiers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import AppKit
import SwiftCrossUI

extension View {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension Button {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSButton) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension Text {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSTextField) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension Slider {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSSlider) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension Picker {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSPopUpButton) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension TextField {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSTextField) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension ScrollView {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSScrollView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}

extension List {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSTableView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSScrollView) in
action(view.documentView as! NSTableView)
}
}
}

extension NavigationSplitView {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSSplitView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (view: NSView) in
action(view.subviews[0] as! NSSplitView)
}
}
}

extension Image {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSImageView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints) { (_: NSView, children: ImageChildren) in
action(children.imageWidget.into())
}
}
}

extension Table {
public func inspect(
_ inspectionPoints: InspectionPoints = .onCreate,
_ action: @escaping @MainActor @Sendable (NSScrollView) -> Void
) -> some View {
InspectView(child: self, inspectionPoints: inspectionPoints, action: action)
}
}
Loading
Loading