From ac82aeb41055b4db9006cd0f990d437da433d8b5 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:25:44 +1000 Subject: [PATCH 01/57] Update Package.swift --- Package.swift | 200 +++++++++++--------------------------------------- 1 file changed, 41 insertions(+), 159 deletions(-) diff --git a/Package.swift b/Package.swift index f24ca7956c..f620333cee 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,15 @@ -// swift-tools-version:5.10 +// swift-tools-version:5.6 -import CompilerPluginSupport import Foundation import PackageDescription -// In Gtk 4.10 some breaking changes were made, so the GtkBackend code needs to know -// which version is in use. +// GTK 4.10+ detection var gtkSwiftSettings: [SwiftSetting] = [] if let version = getGtk4MinorVersion(), version >= 10 { gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) } +// Default backend selection let defaultBackend: String if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { defaultBackend = backend @@ -24,6 +23,7 @@ if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { #endif } +// Hot reloading check let hotReloadingEnabled: Bool #if os(Windows) hotReloadingEnabled = false @@ -33,13 +33,7 @@ let hotReloadingEnabled: Bool || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil #endif -var swiftSettings: [SwiftSetting] = [] -if hotReloadingEnabled { - swiftSettings += [ - .define("HOT_RELOADING_ENABLED") - ] -} - +// Library type var libraryType: Product.Library.LibraryType? switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { case "static": @@ -61,7 +55,12 @@ switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { let package = Package( name: "swift-cross-ui", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13), .visionOS(.v1)], + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .macCatalyst(.v13) + ], products: [ .library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]), .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), @@ -73,61 +72,20 @@ let package = Package( .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), .library(name: "Gtk3", type: libraryType, targets: ["Gtk3"]), .executable(name: "GtkExample", targets: ["GtkExample"]), - // .library(name: "CursesBackend", type: libraryType, targets: ["CursesBackend"]), - // .library(name: "QtBackend", type: libraryType, targets: ["QtBackend"]), - // .library(name: "LVGLBackend", type: libraryType, targets: ["LVGLBackend"]), ], dependencies: [ - .package( - url: "https://github.com/CoreOffice/XMLCoder", - from: "0.17.1" - ), - .package( - url: "https://github.com/swiftlang/swift-docc-plugin", - from: "1.0.0" - ), - .package( - url: "https://github.com/swiftlang/swift-syntax.git", - from: "600.0.0" - ), - .package( - url: "https://github.com/stackotter/swift-macro-toolkit", - .upToNextMinor(from: "0.6.0") - ), - .package( - url: "https://github.com/stackotter/swift-image-formats", - .upToNextMinor(from: "0.3.3") - ), - .package( - url: "https://github.com/stackotter/swift-windowsappsdk", - branch: "ed938db0b9790b36391dc91b20cee81f2410309f" - ), - .package( - url: "https://github.com/thebrowsercompany/swift-windowsfoundation", - branch: "main" - ), - .package( - url: "https://github.com/stackotter/swift-winui", - branch: "927e2c46430cfb1b6c195590b9e65a30a8fd98a2" - ), - // .package( - // url: "https://github.com/stackotter/TermKit", - // revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704" - // ), - // .package( - // url: "https://github.com/PADL/LVGLSwift", - // revision: "19c19a942153b50d61486faf1d0d45daf79e7be5" - // ), - // .package( - // url: "https://github.com/Longhanks/qlift", - // revision: "ddab1f1ecc113ad4f8e05d2999c2734cdf706210" - // ), + .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), + .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), + .package(url: "https://github.com/stackotter/swift-windowsappsdk", branch: "ed938db0b9790b36391dc91b20cee81f2410309f"), + .package(url: "https://github.com/thebrowsercompany/swift-windowsfoundation", branch: "main"), + .package(url: "https://github.com/stackotter/swift-winui", branch: "927e2c46430cfb1b6c195590b9e65a30a8fd98a2"), ], targets: [ .target( name: "SwiftCrossUI", dependencies: [ - "HotReloadingMacrosPlugin", .product(name: "ImageFormats", package: "swift-image-formats"), ], exclude: [ @@ -138,9 +96,6 @@ let package = Package( "Views/TupleViewChildren.swift.gyb", "Views/TableRowContent.swift.gyb", "Scenes/TupleScene.swift.gyb", - ], - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency") ] ), .testTarget( @@ -153,28 +108,13 @@ let package = Package( .target( name: "DefaultBackend", dependencies: [ - .target( - name: defaultBackend, - condition: .when(platforms: [.linux, .macOS, .windows]) - ), - // Non-desktop platforms need to be handled separately: - // Only one backend is supported, and `#if` won't work because it's evaluated - // on the compiling desktop, not the target. - .target( - name: "UIKitBackend", - condition: .when(platforms: [.iOS, .tvOS, .macCatalyst, .visionOS]) - ), + .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS, .windows])), + .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), ] ), .target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]), - .target( - name: "GtkBackend", - dependencies: ["SwiftCrossUI", "Gtk", "CGtk"] - ), - .target( - name: "Gtk3Backend", - dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"] - ), + .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), + .target(name: "Gtk3Backend", dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"]), .systemLibrary( name: "CGtk", pkgConfig: "gtk4", @@ -183,27 +123,10 @@ let package = Package( .apt(["libgtk-4-dev clang"]), ] ), - .target( - name: "Gtk", - dependencies: ["CGtk", "GtkCustomWidgets"], - exclude: ["LICENSE.md"], - swiftSettings: gtkSwiftSettings - ), - .executableTarget( - name: "GtkExample", - dependencies: ["Gtk"], - resources: [.copy("GTK.png")] - ), - .target( - name: "GtkCustomWidgets", - dependencies: ["CGtk"] - ), - .executableTarget( - name: "GtkCodeGen", - dependencies: [ - "XMLCoder", .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), - ] - ), + .target(name: "Gtk", dependencies: ["CGtk", "GtkCustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "GtkExample", dependencies: ["Gtk"], resources: [.copy("GTK.png")]), + .target(name: "GtkCustomWidgets", dependencies: ["CGtk"]), + .executableTarget(name: "GtkCodeGen", dependencies: ["XMLCoder"]), .systemLibrary( name: "CGtk3", pkgConfig: "gtk+-3.0", @@ -212,31 +135,9 @@ let package = Package( .apt(["libgtk-3-dev clang"]), ] ), - .target( - name: "Gtk3", - dependencies: ["CGtk3", "Gtk3CustomWidgets"], - exclude: ["LICENSE.md"], - swiftSettings: gtkSwiftSettings - ), - .executableTarget( - name: "Gtk3Example", - dependencies: ["Gtk3"], - resources: [.copy("GTK.png")] - ), - .target( - name: "Gtk3CustomWidgets", - dependencies: ["CGtk3"] - ), - .macro( - name: "HotReloadingMacrosPlugin", - dependencies: [ - .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), - .product(name: "MacroToolkit", package: "swift-macro-toolkit"), - ], - swiftSettings: swiftSettings - ), + .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), + .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), .target( name: "WinUIBackend", @@ -248,43 +149,24 @@ let package = Package( .product(name: "WindowsFoundation", package: "swift-windowsfoundation"), ] ), - .target( - name: "WinUIInterop", - dependencies: [] - ), - // .target( - // name: "CursesBackend", - // dependencies: ["SwiftCrossUI", "TermKit"] - // ), - // .target( - // name: "QtBackend", - // dependencies: ["SwiftCrossUI", .product(name: "Qlift", package: "qlift")] - // ), - // .target( - // name: "LVGLBackend", - // dependencies: [ - // "SwiftCrossUI", - // .product(name: "LVGL", package: "LVGLSwift"), - // .product(name: "CLVGL", package: "LVGLSwift"), - // ] - // ), + .target(name: "WinUIInterop", dependencies: []), ] ) func getGtk4MinorVersion() -> Int? { #if os(Windows) guard let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], - case let tripletRoot = URL(fileURLWithPath: pkgConfigPath, isDirectory: true) + case let tripletRoot = URL(fileURLWithPath: pkgConfigPath, isDirectory: true) .deletingLastPathComponent().deletingLastPathComponent(), - case let vcpkgInfoDirectory = tripletRoot.deletingLastPathComponent() + case let vcpkgInfoDirectory = tripletRoot.deletingLastPathComponent() .appendingPathComponent("vcpkg").appendingPathComponent("info"), - let installedList = try? FileManager.default.contentsOfDirectory( + let installedList = try? FileManager.default.contentsOfDirectory( at: vcpkgInfoDirectory, includingPropertiesForKeys: nil - ) - .map({ $0.deletingPathExtension().lastPathComponent }), - let packageName = installedList.first(where: { - $0.hasPrefix("gtk_") && $0.hasSuffix("_\(tripletRoot.lastPathComponent)") - }) + ) + .map({ $0.deletingPathExtension().lastPathComponent }), + let packageName = installedList.first(where: { + $0.hasPrefix("gtk_") && $0.hasSuffix("_\(tripletRoot.lastPathComponent)") + }) else { print("We only support installing gtk through vcpkg on Windows.") return nil @@ -299,9 +181,9 @@ func getGtk4MinorVersion() -> Int? { process.standardOutput = pipe guard (try? process.run()) != nil, - let data = try? pipe.fileHandleForReading.readToEnd(), - case _ = process.waitUntilExit(), - let version = String(data: data, encoding: .utf8)?.split(separator: ".") + let data = try? pipe.fileHandleForReading.readToEnd(), + case _ = process.waitUntilExit(), + let version = String(data: data, encoding: .utf8)?.split(separator: ".") else { print("Failed to get gtk version") return nil From b8cd2b8ad2bc050c8124a1cdecb49938dadd06e6 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:31:51 +1000 Subject: [PATCH 02/57] Update Package.swift --- Package.swift | 54 ++++++--------------------------------------------- 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/Package.swift b/Package.swift index f620333cee..e3e011ee9c 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,6 @@ if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { } else { #if os(macOS) defaultBackend = "AppKitBackend" - #elseif os(Windows) - defaultBackend = "WinUIBackend" #else defaultBackend = "GtkBackend" #endif @@ -25,13 +23,9 @@ if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { // Hot reloading check let hotReloadingEnabled: Bool -#if os(Windows) - hotReloadingEnabled = false -#else - hotReloadingEnabled = - ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil - || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil -#endif +hotReloadingEnabled = + ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil + || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil // Library type var libraryType: Product.Library.LibraryType? @@ -46,11 +40,7 @@ switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto") libraryType = nil case nil: - if hotReloadingEnabled { - libraryType = .dynamic - } else { - libraryType = nil - } + libraryType = hotReloadingEnabled ? .dynamic : nil } let package = Package( @@ -66,7 +56,6 @@ let package = Package( .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), .library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]), .library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]), - .library(name: "WinUIBackend", type: libraryType, targets: ["WinUIBackend"]), .library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]), .library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]), .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), @@ -78,9 +67,6 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), - .package(url: "https://github.com/stackotter/swift-windowsappsdk", branch: "ed938db0b9790b36391dc91b20cee81f2410309f"), - .package(url: "https://github.com/thebrowsercompany/swift-windowsfoundation", branch: "main"), - .package(url: "https://github.com/stackotter/swift-winui", branch: "927e2c46430cfb1b6c195590b9e65a30a8fd98a2"), ], targets: [ .target( @@ -108,7 +94,7 @@ let package = Package( .target( name: "DefaultBackend", dependencies: [ - .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS, .windows])), + .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS])), .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), ] ), @@ -139,40 +125,12 @@ let package = Package( .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), - .target( - name: "WinUIBackend", - dependencies: [ - "SwiftCrossUI", - "WinUIInterop", - .product(name: "WinUI", package: "swift-winui"), - .product(name: "WinAppSDK", package: "swift-windowsappsdk"), - .product(name: "WindowsFoundation", package: "swift-windowsfoundation"), - ] - ), - .target(name: "WinUIInterop", dependencies: []), ] ) func getGtk4MinorVersion() -> Int? { #if os(Windows) - guard let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], - case let tripletRoot = URL(fileURLWithPath: pkgConfigPath, isDirectory: true) - .deletingLastPathComponent().deletingLastPathComponent(), - case let vcpkgInfoDirectory = tripletRoot.deletingLastPathComponent() - .appendingPathComponent("vcpkg").appendingPathComponent("info"), - let installedList = try? FileManager.default.contentsOfDirectory( - at: vcpkgInfoDirectory, includingPropertiesForKeys: nil - ) - .map({ $0.deletingPathExtension().lastPathComponent }), - let packageName = installedList.first(where: { - $0.hasPrefix("gtk_") && $0.hasSuffix("_\(tripletRoot.lastPathComponent)") - }) - else { - print("We only support installing gtk through vcpkg on Windows.") - return nil - } - - let version = packageName.split(separator: "_")[1].split(separator: ".") + return nil // Windows backend removed #else let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/bash") From 34abd1f2391c6708be61c283e0406671fe40a35a Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:34:04 +1000 Subject: [PATCH 03/57] Update Package.swift --- Package.swift | 384 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 274 insertions(+), 110 deletions(-) diff --git a/Package.swift b/Package.swift index e3e011ee9c..4877f3c801 100644 --- a/Package.swift +++ b/Package.swift @@ -1,136 +1,300 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.5 import Foundation import PackageDescription -// GTK 4.10+ detection -var gtkSwiftSettings: [SwiftSetting] = [] -if let version = getGtk4MinorVersion(), version >= 10 { - gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) +var dependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/CoreOffice/XMLCoder", + from: "0.17.1" + ), + .package( + url: "https://github.com/apple/swift-syntax.git", + from: "508.0.0" + ), +] + +#if swift(>=5.6) && !os(Windows) + // Add the documentation compiler plugin if possible + dependencies.append( + .package( + url: "https://github.com/apple/swift-docc-plugin", + from: "1.0.0" + ) + ) +#endif + +#if swift(<5.8) && os(Windows) + if let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], + pkgConfigPath.contains(":") + { + print("PKG_CONFIG_PATH might not be parsed correctly with your Swift tools version.") + print("Upgrade to Swift 5.8+ instead.") + } +#endif + +#if !os(Linux) && !os(macOS) && !os(Windows) + fatalError("Unsupported platform.") +#endif + +var conditionalProducts: [Product] = [] +var conditionalTargets: [Target] = [] +var exampleDependencies: [Target.Dependency] = ["SwiftCrossUI", "GtkBackend"] +var fileViewerExampleDependencies: [Target.Dependency] = ["SwiftCrossUI", "GtkBackend", "CGtk"] +var backendTargets: [String] = [] + +// If Gtk is detected, add Gtk-related products and targets +if let version = getGtk4MinorVersion() { + var gtkSwiftSettings: [SwiftSetting] = [] + var gtkExampleDependencies: [Target.Dependency] = ["Gtk"] + backendTargets.append("GtkBackend") + fileViewerExampleDependencies.append("GtkBackend") + + // Conditionally enable features that rely on Gtk 4.10 + if version >= 10 { + conditionalTargets.append( + .target(name: "FileDialog", dependencies: ["CGtk", "Gtk"]) + ) + + gtkExampleDependencies.append("FileDialog") + fileViewerExampleDependencies.append("FileDialog") + gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) + } + + conditionalProducts.append( + contentsOf: [ + .library(name: "GtkBackend", targets: ["GtkBackend"]), + .library(name: "Gtk", targets: ["Gtk"]), + .executable(name: "GtkExample", targets: ["GtkExample"]), + ] + ) + + conditionalTargets.append( + contentsOf: [ + .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), + .systemLibrary( + name: "CGtk", + pkgConfig: "gtk4", + providers: [ + .brew(["gtk4"]), + .apt(["libgtk-4-dev clang"]), + ] + ), + .target(name: "Gtk", dependencies: ["CGtk"], swiftSettings: gtkSwiftSettings), + .executableTarget( + name: "GtkExample", + dependencies: gtkExampleDependencies, + resources: [.copy("GTK.png")] + ), + + .executableTarget( + name: "GtkCodeGen", + dependencies: [ + "XMLCoder", .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + ] + ), + ] + ) } -// Default backend selection -let defaultBackend: String -if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { - defaultBackend = backend -} else { - #if os(macOS) - defaultBackend = "AppKitBackend" - #else - defaultBackend = "GtkBackend" - #endif +#if canImport(AppKit) + conditionalTargets.append(.target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"])) + backendTargets.append("AppKitBackend") +#endif + +if checkQtInstalled() { + conditionalTargets.append( + .target( + name: "QtBackend", + dependencies: ["SwiftCrossUI", .product(name: "Qlift", package: "qlift")] + ) + ) + backendTargets.append("QtBackend") + dependencies.append( + .package( + url: "https://github.com/Longhanks/qlift", + revision: "ddab1f1ecc113ad4f8e05d2999c2734cdf706210" + ) + ) } -// Hot reloading check -let hotReloadingEnabled: Bool -hotReloadingEnabled = - ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil - || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil - -// Library type -var libraryType: Product.Library.LibraryType? -switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { - case "static": - libraryType = .static - case "dynamic": - libraryType = .dynamic - case "auto": - libraryType = nil - case .some: - print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto") - libraryType = nil - case nil: - libraryType = hotReloadingEnabled ? .dynamic : nil +if checkSDL2Installed() { + conditionalTargets.append( + .target( + name: "LVGLBackend", + dependencies: [ + "SwiftCrossUI", + .product(name: "LVGL", package: "LVGLSwift"), + .product(name: "CLVGL", package: "LVGLSwift"), + ] + ) + ) + backendTargets.append("LVGLBackend") + dependencies.append( + .package( + url: "https://github.com/PADL/LVGLSwift", + revision: "19c19a942153b50d61486faf1d0d45daf79e7be5" + ) + ) } +#if os(macOS) + // TODO: Switch to a different terminal library that doesn't have issues on non-Apple platforms + conditionalTargets.append( + .target( + name: "CursesBackend", + dependencies: ["SwiftCrossUI", "TermKit"] + ) + ) + backendTargets.append("CursesBackend") + dependencies.append( + .package( + url: "https://github.com/migueldeicaza/TermKit", + revision: "3bce85d1bafbbb0336b3b7b7e905c35754cb9adf" + ) + ) +#endif + let package = Package( name: "swift-cross-ui", - platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .tvOS(.v13), - .macCatalyst(.v13) - ], + platforms: [.macOS(.v10_15)], products: [ - .library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]), - .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), - .library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]), - .library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]), - .library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]), - .library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]), - .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), - .library(name: "Gtk3", type: libraryType, targets: ["Gtk3"]), - .executable(name: "GtkExample", targets: ["GtkExample"]), - ], - dependencies: [ - .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), - .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), - ], + .library(name: "SwiftCrossUI", targets: ["SwiftCrossUI"] + backendTargets), + .executable(name: "CounterExample", targets: ["CounterExample"]), + .executable( + name: "RandomNumberGeneratorExample", + targets: ["RandomNumberGeneratorExample"] + ), + .executable( + name: "WindowPropertiesExample", + targets: ["WindowPropertiesExample"] + ), + .executable( + name: "GreetingGeneratorExample", + targets: ["GreetingGeneratorExample"] + ), + .executable(name: "FileViewerExample", targets: ["FileViewerExample"]), + .executable(name: "NavigationExample", targets: ["NavigationExample"]), + .executable(name: "SplitExample", targets: ["SplitExample"]), + ] + conditionalProducts, + dependencies: dependencies, targets: [ .target( name: "SwiftCrossUI", - dependencies: [ - .product(name: "ImageFormats", package: "swift-image-formats"), - ], + dependencies: [], exclude: [ - "Builders/ViewBuilder.swift.gyb", - "Builders/SceneBuilder.swift.gyb", - "Builders/TableRowBuilder.swift.gyb", - "Views/TupleView.swift.gyb", - "Views/TupleViewChildren.swift.gyb", - "Views/TableRowContent.swift.gyb", - "Scenes/TupleScene.swift.gyb", + "Builders/ViewContentBuilder.swift.gyb", + "ViewGraph/ViewGraphNodeChildren.swift.gyb", + "Views/ViewContent.swift.gyb", ] ), - .testTarget( - name: "SwiftCrossUITests", - dependencies: [ - "SwiftCrossUI", - .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), - ] + .executableTarget( + name: "CounterExample", + dependencies: exampleDependencies, + path: "Examples/Counter" ), - .target( - name: "DefaultBackend", - dependencies: [ - .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS])), - .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), - ] + .executableTarget( + name: "RandomNumberGeneratorExample", + dependencies: exampleDependencies, + path: "Examples/RandomNumberGenerator" ), - .target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]), - .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), - .target(name: "Gtk3Backend", dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"]), - .systemLibrary( - name: "CGtk", - pkgConfig: "gtk4", - providers: [ - .brew(["gtk4"]), - .apt(["libgtk-4-dev clang"]), - ] + .executableTarget( + name: "WindowPropertiesExample", + dependencies: exampleDependencies, + path: "Examples/WindowProperties", + resources: [.copy("Banner.png")] ), - .target(name: "Gtk", dependencies: ["CGtk", "GtkCustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), - .executableTarget(name: "GtkExample", dependencies: ["Gtk"], resources: [.copy("GTK.png")]), - .target(name: "GtkCustomWidgets", dependencies: ["CGtk"]), - .executableTarget(name: "GtkCodeGen", dependencies: ["XMLCoder"]), - .systemLibrary( - name: "CGtk3", - pkgConfig: "gtk+-3.0", - providers: [ - .brew(["gtk+3"]), - .apt(["libgtk-3-dev clang"]), - ] + .executableTarget( + name: "GreetingGeneratorExample", + dependencies: exampleDependencies, + path: "Examples/GreetingGenerator" + ), + .executableTarget( + name: "FileViewerExample", + dependencies: fileViewerExampleDependencies, + path: "Examples/FileViewer" + ), + .executableTarget( + name: "NavigationExample", + dependencies: exampleDependencies, + path: "Examples/Navigation" + ), + .executableTarget( + name: "SplitExample", + dependencies: exampleDependencies, + path: "Examples/Split" + ), + .executableTarget( + name: "SpreadsheetExample", + dependencies: exampleDependencies, + path: "Examples/Spreadsheet" ), - .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), - .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), - .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), - .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), - ] + + .testTarget( + name: "SwiftCrossUITests", + dependencies: ["SwiftCrossUI"] + ), + ] + conditionalTargets ) +func checkQtInstalled() -> Bool { + #if os(Windows) + // TODO: Test Qt backend on Windows + return false + #else + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["-c", "qmake --version"] + process.standardOutput = Pipe() + do { + try process.run() + process.waitUntilExit() + return process.terminationStatus == 0 + } catch { + return false + } + #endif +} + +func checkSDL2Installed() -> Bool { + #if os(Windows) + // TODO: Test SDL backend on Windows + return false + #else + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["-c", "sdl2-config --version"] + process.standardOutput = Pipe() + do { + try process.run() + process.waitUntilExit() + return process.terminationStatus == 0 + } catch { + return false + } + #endif +} + func getGtk4MinorVersion() -> Int? { #if os(Windows) - return nil // Windows backend removed + guard let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], + case let tripletRoot = URL(fileURLWithPath: pkgConfigPath, isDirectory: true) + .deletingLastPathComponent().deletingLastPathComponent(), + case let vcpkgInfoDirectory = tripletRoot.deletingLastPathComponent() + .appendingPathComponent("vcpkg").appendingPathComponent("info"), + let installedList = try? FileManager.default.contentsOfDirectory( + at: vcpkgInfoDirectory, includingPropertiesForKeys: nil + ) + .map({ $0.deletingPathExtension().lastPathComponent }), + let packageName = installedList.first(where: { + $0.hasPrefix("gtk_") && $0.hasSuffix("_\(tripletRoot.lastPathComponent)") + }) + else { + print("We only support installing gtk through vcpkg on Windows.") + return nil + } + + let version = packageName.split(separator: "_")[1].split(separator: ".") #else let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/bash") @@ -138,10 +302,10 @@ func getGtk4MinorVersion() -> Int? { let pipe = Pipe() process.standardOutput = pipe - guard (try? process.run()) != nil, - let data = try? pipe.fileHandleForReading.readToEnd(), - case _ = process.waitUntilExit(), - let version = String(data: data, encoding: .utf8)?.split(separator: ".") + guard let _ = try? process.run(), + let data = try? pipe.fileHandleForReading.readToEnd(), + case _ = process.waitUntilExit(), + let version = String(data: data, encoding: .utf8)?.split(separator: ".") else { print("Failed to get gtk version") return nil From c4d515a34107a02c6201cd35e076027064fe2b47 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:35:37 +1000 Subject: [PATCH 04/57] Update Package.swift --- Package.swift | 384 +++++++++++++++----------------------------------- 1 file changed, 110 insertions(+), 274 deletions(-) diff --git a/Package.swift b/Package.swift index 4877f3c801..e3e011ee9c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,300 +1,136 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.6 import Foundation import PackageDescription -var dependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/CoreOffice/XMLCoder", - from: "0.17.1" - ), - .package( - url: "https://github.com/apple/swift-syntax.git", - from: "508.0.0" - ), -] - -#if swift(>=5.6) && !os(Windows) - // Add the documentation compiler plugin if possible - dependencies.append( - .package( - url: "https://github.com/apple/swift-docc-plugin", - from: "1.0.0" - ) - ) -#endif - -#if swift(<5.8) && os(Windows) - if let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], - pkgConfigPath.contains(":") - { - print("PKG_CONFIG_PATH might not be parsed correctly with your Swift tools version.") - print("Upgrade to Swift 5.8+ instead.") - } -#endif - -#if !os(Linux) && !os(macOS) && !os(Windows) - fatalError("Unsupported platform.") -#endif - -var conditionalProducts: [Product] = [] -var conditionalTargets: [Target] = [] -var exampleDependencies: [Target.Dependency] = ["SwiftCrossUI", "GtkBackend"] -var fileViewerExampleDependencies: [Target.Dependency] = ["SwiftCrossUI", "GtkBackend", "CGtk"] -var backendTargets: [String] = [] - -// If Gtk is detected, add Gtk-related products and targets -if let version = getGtk4MinorVersion() { - var gtkSwiftSettings: [SwiftSetting] = [] - var gtkExampleDependencies: [Target.Dependency] = ["Gtk"] - backendTargets.append("GtkBackend") - fileViewerExampleDependencies.append("GtkBackend") - - // Conditionally enable features that rely on Gtk 4.10 - if version >= 10 { - conditionalTargets.append( - .target(name: "FileDialog", dependencies: ["CGtk", "Gtk"]) - ) - - gtkExampleDependencies.append("FileDialog") - fileViewerExampleDependencies.append("FileDialog") - gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) - } - - conditionalProducts.append( - contentsOf: [ - .library(name: "GtkBackend", targets: ["GtkBackend"]), - .library(name: "Gtk", targets: ["Gtk"]), - .executable(name: "GtkExample", targets: ["GtkExample"]), - ] - ) - - conditionalTargets.append( - contentsOf: [ - .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), - .systemLibrary( - name: "CGtk", - pkgConfig: "gtk4", - providers: [ - .brew(["gtk4"]), - .apt(["libgtk-4-dev clang"]), - ] - ), - .target(name: "Gtk", dependencies: ["CGtk"], swiftSettings: gtkSwiftSettings), - .executableTarget( - name: "GtkExample", - dependencies: gtkExampleDependencies, - resources: [.copy("GTK.png")] - ), - - .executableTarget( - name: "GtkCodeGen", - dependencies: [ - "XMLCoder", .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), - ] - ), - ] - ) +// GTK 4.10+ detection +var gtkSwiftSettings: [SwiftSetting] = [] +if let version = getGtk4MinorVersion(), version >= 10 { + gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) } -#if canImport(AppKit) - conditionalTargets.append(.target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"])) - backendTargets.append("AppKitBackend") -#endif - -if checkQtInstalled() { - conditionalTargets.append( - .target( - name: "QtBackend", - dependencies: ["SwiftCrossUI", .product(name: "Qlift", package: "qlift")] - ) - ) - backendTargets.append("QtBackend") - dependencies.append( - .package( - url: "https://github.com/Longhanks/qlift", - revision: "ddab1f1ecc113ad4f8e05d2999c2734cdf706210" - ) - ) +// Default backend selection +let defaultBackend: String +if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { + defaultBackend = backend +} else { + #if os(macOS) + defaultBackend = "AppKitBackend" + #else + defaultBackend = "GtkBackend" + #endif } -if checkSDL2Installed() { - conditionalTargets.append( - .target( - name: "LVGLBackend", - dependencies: [ - "SwiftCrossUI", - .product(name: "LVGL", package: "LVGLSwift"), - .product(name: "CLVGL", package: "LVGLSwift"), - ] - ) - ) - backendTargets.append("LVGLBackend") - dependencies.append( - .package( - url: "https://github.com/PADL/LVGLSwift", - revision: "19c19a942153b50d61486faf1d0d45daf79e7be5" - ) - ) +// Hot reloading check +let hotReloadingEnabled: Bool +hotReloadingEnabled = + ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil + || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil + +// Library type +var libraryType: Product.Library.LibraryType? +switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { + case "static": + libraryType = .static + case "dynamic": + libraryType = .dynamic + case "auto": + libraryType = nil + case .some: + print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto") + libraryType = nil + case nil: + libraryType = hotReloadingEnabled ? .dynamic : nil } -#if os(macOS) - // TODO: Switch to a different terminal library that doesn't have issues on non-Apple platforms - conditionalTargets.append( - .target( - name: "CursesBackend", - dependencies: ["SwiftCrossUI", "TermKit"] - ) - ) - backendTargets.append("CursesBackend") - dependencies.append( - .package( - url: "https://github.com/migueldeicaza/TermKit", - revision: "3bce85d1bafbbb0336b3b7b7e905c35754cb9adf" - ) - ) -#endif - let package = Package( name: "swift-cross-ui", - platforms: [.macOS(.v10_15)], + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .macCatalyst(.v13) + ], products: [ - .library(name: "SwiftCrossUI", targets: ["SwiftCrossUI"] + backendTargets), - .executable(name: "CounterExample", targets: ["CounterExample"]), - .executable( - name: "RandomNumberGeneratorExample", - targets: ["RandomNumberGeneratorExample"] - ), - .executable( - name: "WindowPropertiesExample", - targets: ["WindowPropertiesExample"] - ), - .executable( - name: "GreetingGeneratorExample", - targets: ["GreetingGeneratorExample"] - ), - .executable(name: "FileViewerExample", targets: ["FileViewerExample"]), - .executable(name: "NavigationExample", targets: ["NavigationExample"]), - .executable(name: "SplitExample", targets: ["SplitExample"]), - ] + conditionalProducts, - dependencies: dependencies, + .library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]), + .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), + .library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]), + .library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]), + .library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]), + .library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]), + .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), + .library(name: "Gtk3", type: libraryType, targets: ["Gtk3"]), + .executable(name: "GtkExample", targets: ["GtkExample"]), + ], + dependencies: [ + .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), + .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), + ], targets: [ .target( name: "SwiftCrossUI", - dependencies: [], + dependencies: [ + .product(name: "ImageFormats", package: "swift-image-formats"), + ], exclude: [ - "Builders/ViewContentBuilder.swift.gyb", - "ViewGraph/ViewGraphNodeChildren.swift.gyb", - "Views/ViewContent.swift.gyb", + "Builders/ViewBuilder.swift.gyb", + "Builders/SceneBuilder.swift.gyb", + "Builders/TableRowBuilder.swift.gyb", + "Views/TupleView.swift.gyb", + "Views/TupleViewChildren.swift.gyb", + "Views/TableRowContent.swift.gyb", + "Scenes/TupleScene.swift.gyb", ] ), - .executableTarget( - name: "CounterExample", - dependencies: exampleDependencies, - path: "Examples/Counter" - ), - .executableTarget( - name: "RandomNumberGeneratorExample", - dependencies: exampleDependencies, - path: "Examples/RandomNumberGenerator" - ), - .executableTarget( - name: "WindowPropertiesExample", - dependencies: exampleDependencies, - path: "Examples/WindowProperties", - resources: [.copy("Banner.png")] - ), - .executableTarget( - name: "GreetingGeneratorExample", - dependencies: exampleDependencies, - path: "Examples/GreetingGenerator" - ), - .executableTarget( - name: "FileViewerExample", - dependencies: fileViewerExampleDependencies, - path: "Examples/FileViewer" - ), - .executableTarget( - name: "NavigationExample", - dependencies: exampleDependencies, - path: "Examples/Navigation" + .testTarget( + name: "SwiftCrossUITests", + dependencies: [ + "SwiftCrossUI", + .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), + ] ), - .executableTarget( - name: "SplitExample", - dependencies: exampleDependencies, - path: "Examples/Split" + .target( + name: "DefaultBackend", + dependencies: [ + .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS])), + .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), + ] ), - .executableTarget( - name: "SpreadsheetExample", - dependencies: exampleDependencies, - path: "Examples/Spreadsheet" + .target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]), + .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), + .target(name: "Gtk3Backend", dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"]), + .systemLibrary( + name: "CGtk", + pkgConfig: "gtk4", + providers: [ + .brew(["gtk4"]), + .apt(["libgtk-4-dev clang"]), + ] ), - - .testTarget( - name: "SwiftCrossUITests", - dependencies: ["SwiftCrossUI"] + .target(name: "Gtk", dependencies: ["CGtk", "GtkCustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "GtkExample", dependencies: ["Gtk"], resources: [.copy("GTK.png")]), + .target(name: "GtkCustomWidgets", dependencies: ["CGtk"]), + .executableTarget(name: "GtkCodeGen", dependencies: ["XMLCoder"]), + .systemLibrary( + name: "CGtk3", + pkgConfig: "gtk+-3.0", + providers: [ + .brew(["gtk+3"]), + .apt(["libgtk-3-dev clang"]), + ] ), - ] + conditionalTargets + .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), + .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), + .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), + ] ) -func checkQtInstalled() -> Bool { - #if os(Windows) - // TODO: Test Qt backend on Windows - return false - #else - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/bash") - process.arguments = ["-c", "qmake --version"] - process.standardOutput = Pipe() - do { - try process.run() - process.waitUntilExit() - return process.terminationStatus == 0 - } catch { - return false - } - #endif -} - -func checkSDL2Installed() -> Bool { - #if os(Windows) - // TODO: Test SDL backend on Windows - return false - #else - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/bash") - process.arguments = ["-c", "sdl2-config --version"] - process.standardOutput = Pipe() - do { - try process.run() - process.waitUntilExit() - return process.terminationStatus == 0 - } catch { - return false - } - #endif -} - func getGtk4MinorVersion() -> Int? { #if os(Windows) - guard let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"], - case let tripletRoot = URL(fileURLWithPath: pkgConfigPath, isDirectory: true) - .deletingLastPathComponent().deletingLastPathComponent(), - case let vcpkgInfoDirectory = tripletRoot.deletingLastPathComponent() - .appendingPathComponent("vcpkg").appendingPathComponent("info"), - let installedList = try? FileManager.default.contentsOfDirectory( - at: vcpkgInfoDirectory, includingPropertiesForKeys: nil - ) - .map({ $0.deletingPathExtension().lastPathComponent }), - let packageName = installedList.first(where: { - $0.hasPrefix("gtk_") && $0.hasSuffix("_\(tripletRoot.lastPathComponent)") - }) - else { - print("We only support installing gtk through vcpkg on Windows.") - return nil - } - - let version = packageName.split(separator: "_")[1].split(separator: ".") + return nil // Windows backend removed #else let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/bash") @@ -302,10 +138,10 @@ func getGtk4MinorVersion() -> Int? { let pipe = Pipe() process.standardOutput = pipe - guard let _ = try? process.run(), - let data = try? pipe.fileHandleForReading.readToEnd(), - case _ = process.waitUntilExit(), - let version = String(data: data, encoding: .utf8)?.split(separator: ".") + guard (try? process.run()) != nil, + let data = try? pipe.fileHandleForReading.readToEnd(), + case _ = process.waitUntilExit(), + let version = String(data: data, encoding: .utf8)?.split(separator: ".") else { print("Failed to get gtk version") return nil From 086da4638c54c999984a5a33ae435e66aea913f1 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:38:48 +1000 Subject: [PATCH 05/57] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index e3e011ee9c..2b980f2059 100644 --- a/Package.swift +++ b/Package.swift @@ -65,7 +65,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0"), .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), ], targets: [ From 3d83cb415ec9e1d1da0df00c76d7ecbbe13f3a0c Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:28:28 +1000 Subject: [PATCH 06/57] Update Package.swift --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 2b980f2059..3b2c581e5b 100644 --- a/Package.swift +++ b/Package.swift @@ -65,8 +65,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0"), - .package(url: "https://github.com/stackotter/swift-image-formats", .upToNextMinor(from: "0.3.3")), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0")), ], targets: [ .target( From cba85347cda6a3061f3bafed89014a50040252d4 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:29:56 +1000 Subject: [PATCH 07/57] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 3b2c581e5b..cf0c4a44d7 100644 --- a/Package.swift +++ b/Package.swift @@ -65,7 +65,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0")), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0")) ], targets: [ .target( From 9076dd535e4808927e043cbed794dbbf4d08fa70 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:31:44 +1000 Subject: [PATCH 08/57] Update Package.swift --- Package.swift | 119 +++++--------------------------------------------- 1 file changed, 11 insertions(+), 108 deletions(-) diff --git a/Package.swift b/Package.swift index cf0c4a44d7..51d0ba8923 100644 --- a/Package.swift +++ b/Package.swift @@ -1,48 +1,6 @@ // swift-tools-version:5.6 - -import Foundation import PackageDescription -// GTK 4.10+ detection -var gtkSwiftSettings: [SwiftSetting] = [] -if let version = getGtk4MinorVersion(), version >= 10 { - gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) -} - -// Default backend selection -let defaultBackend: String -if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { - defaultBackend = backend -} else { - #if os(macOS) - defaultBackend = "AppKitBackend" - #else - defaultBackend = "GtkBackend" - #endif -} - -// Hot reloading check -let hotReloadingEnabled: Bool -hotReloadingEnabled = - ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil - || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil - -// Library type -var libraryType: Product.Library.LibraryType? -switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { - case "static": - libraryType = .static - case "dynamic": - libraryType = .dynamic - case "auto": - libraryType = nil - case .some: - print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto") - libraryType = nil - case nil: - libraryType = hotReloadingEnabled ? .dynamic : nil -} - let package = Package( name: "swift-cross-ui", platforms: [ @@ -52,54 +10,41 @@ let package = Package( .macCatalyst(.v13) ], products: [ - .library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]), - .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), - .library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]), - .library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]), - .library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]), - .library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]), - .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), - .library(name: "Gtk3", type: libraryType, targets: ["Gtk3"]), + .library(name: "SwiftCrossUI", targets: ["SwiftCrossUI"]), + .library(name: "AppKitBackend", targets: ["AppKitBackend"]), + .library(name: "GtkBackend", targets: ["GtkBackend"]), + .library(name: "Gtk3Backend", targets: ["Gtk3Backend"]), + .library(name: "DefaultBackend", targets: ["DefaultBackend"]), + .library(name: "UIKitBackend", targets: ["UIKitBackend"]), + .library(name: "Gtk", targets: ["Gtk"]), + .library(name: "Gtk3", targets: ["Gtk3"]), .executable(name: "GtkExample", targets: ["GtkExample"]), ], dependencies: [ .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0")) + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0"), ], targets: [ .target( name: "SwiftCrossUI", dependencies: [ .product(name: "ImageFormats", package: "swift-image-formats"), - ], - exclude: [ - "Builders/ViewBuilder.swift.gyb", - "Builders/SceneBuilder.swift.gyb", - "Builders/TableRowBuilder.swift.gyb", - "Views/TupleView.swift.gyb", - "Views/TupleViewChildren.swift.gyb", - "Views/TableRowContent.swift.gyb", - "Scenes/TupleScene.swift.gyb", ] ), .testTarget( name: "SwiftCrossUITests", - dependencies: [ - "SwiftCrossUI", - .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), - ] + dependencies: ["SwiftCrossUI"] ), .target( name: "DefaultBackend", dependencies: [ - .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS])), + .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), ] ), .target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]), .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), - .target(name: "Gtk3Backend", dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"]), .systemLibrary( name: "CGtk", pkgConfig: "gtk4", @@ -108,47 +53,5 @@ let package = Package( .apt(["libgtk-4-dev clang"]), ] ), - .target(name: "Gtk", dependencies: ["CGtk", "GtkCustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), - .executableTarget(name: "GtkExample", dependencies: ["Gtk"], resources: [.copy("GTK.png")]), - .target(name: "GtkCustomWidgets", dependencies: ["CGtk"]), - .executableTarget(name: "GtkCodeGen", dependencies: ["XMLCoder"]), - .systemLibrary( - name: "CGtk3", - pkgConfig: "gtk+-3.0", - providers: [ - .brew(["gtk+3"]), - .apt(["libgtk-3-dev clang"]), - ] - ), - .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), - .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), - .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), - .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), ] ) - -func getGtk4MinorVersion() -> Int? { - #if os(Windows) - return nil // Windows backend removed - #else - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/bash") - process.arguments = ["-c", "gtk4-launch --version"] - let pipe = Pipe() - process.standardOutput = pipe - - guard (try? process.run()) != nil, - let data = try? pipe.fileHandleForReading.readToEnd(), - case _ = process.waitUntilExit(), - let version = String(data: data, encoding: .utf8)?.split(separator: ".") - else { - print("Failed to get gtk version") - return nil - } - #endif - guard version.count >= 2, let minor = Int(version[1]) else { - print("Failed to get gtk version") - return nil - } - return minor -} From 48f1a0a834f000e741499324a6cd8af242af527b Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:32:52 +1000 Subject: [PATCH 09/57] Update Package.swift --- Package.swift | 117 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index 51d0ba8923..8144de5e5b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,48 @@ // swift-tools-version:5.6 + +import Foundation import PackageDescription +// GTK 4.10+ detection +var gtkSwiftSettings: [SwiftSetting] = [] +if let version = getGtk4MinorVersion(), version >= 10 { + gtkSwiftSettings.append(.define("GTK_4_10_PLUS")) +} + +// Default backend selection +let defaultBackend: String +if let backend = ProcessInfo.processInfo.environment["SCUI_DEFAULT_BACKEND"] { + defaultBackend = backend +} else { + #if os(macOS) + defaultBackend = "AppKitBackend" + #else + defaultBackend = "GtkBackend" + #endif +} + +// Hot reloading check +let hotReloadingEnabled: Bool +hotReloadingEnabled = + ProcessInfo.processInfo.environment["SWIFT_BUNDLER_HOT_RELOADING"] != nil + || ProcessInfo.processInfo.environment["SCUI_HOT_RELOADING"] != nil + +// Library type +var libraryType: Product.Library.LibraryType? +switch ProcessInfo.processInfo.environment["SCUI_LIBRARY_TYPE"] { + case "static": + libraryType = .static + case "dynamic": + libraryType = .dynamic + case "auto": + libraryType = nil + case .some: + print("Invalid SCUI_LIBRARY_TYPE, expected static, dynamic, or auto") + libraryType = nil + case nil: + libraryType = hotReloadingEnabled ? .dynamic : nil +} + let package = Package( name: "swift-cross-ui", platforms: [ @@ -10,14 +52,14 @@ let package = Package( .macCatalyst(.v13) ], products: [ - .library(name: "SwiftCrossUI", targets: ["SwiftCrossUI"]), - .library(name: "AppKitBackend", targets: ["AppKitBackend"]), - .library(name: "GtkBackend", targets: ["GtkBackend"]), - .library(name: "Gtk3Backend", targets: ["Gtk3Backend"]), - .library(name: "DefaultBackend", targets: ["DefaultBackend"]), - .library(name: "UIKitBackend", targets: ["UIKitBackend"]), - .library(name: "Gtk", targets: ["Gtk"]), - .library(name: "Gtk3", targets: ["Gtk3"]), + .library(name: "SwiftCrossUI", type: libraryType, targets: ["SwiftCrossUI"]), + .library(name: "AppKitBackend", type: libraryType, targets: ["AppKitBackend"]), + .library(name: "GtkBackend", type: libraryType, targets: ["GtkBackend"]), + .library(name: "Gtk3Backend", type: libraryType, targets: ["Gtk3Backend"]), + .library(name: "DefaultBackend", type: libraryType, targets: ["DefaultBackend"]), + .library(name: "UIKitBackend", type: libraryType, targets: ["UIKitBackend"]), + .library(name: "Gtk", type: libraryType, targets: ["Gtk"]), + .library(name: "Gtk3", type: libraryType, targets: ["Gtk3"]), .executable(name: "GtkExample", targets: ["GtkExample"]), ], dependencies: [ @@ -30,21 +72,34 @@ let package = Package( name: "SwiftCrossUI", dependencies: [ .product(name: "ImageFormats", package: "swift-image-formats"), + ], + exclude: [ + "Builders/ViewBuilder.swift.gyb", + "Builders/SceneBuilder.swift.gyb", + "Builders/TableRowBuilder.swift.gyb", + "Views/TupleView.swift.gyb", + "Views/TupleViewChildren.swift.gyb", + "Views/TableRowContent.swift.gyb", + "Scenes/TupleScene.swift.gyb", ] ), .testTarget( name: "SwiftCrossUITests", - dependencies: ["SwiftCrossUI"] + dependencies: [ + "SwiftCrossUI", + .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), + ] ), .target( name: "DefaultBackend", dependencies: [ - .target(name: "AppKitBackend", condition: .when(platforms: [.macOS])), + .target(name: defaultBackend, condition: .when(platforms: [.linux, .macOS])), .target(name: "UIKitBackend", condition: .when(platforms: [.iOS, .tvOS, .macCatalyst])), ] ), .target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]), .target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]), + .target(name: "Gtk3Backend", dependencies: ["SwiftCrossUI", "Gtk3", "CGtk3"]), .systemLibrary( name: "CGtk", pkgConfig: "gtk4", @@ -53,5 +108,47 @@ let package = Package( .apt(["libgtk-4-dev clang"]), ] ), + .target(name: "Gtk", dependencies: ["CGtk", "GtkCustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "GtkExample", dependencies: ["Gtk"], resources: [.copy("GTK.png")]), + .target(name: "GtkCustomWidgets", dependencies: ["CGtk"]), + .executableTarget(name: "GtkCodeGen", dependencies: ["XMLCoder"]), + .systemLibrary( + name: "CGtk3", + pkgConfig: "gtk+-3.0", + providers: [ + .brew(["gtk+3"]), + .apt(["libgtk-3-dev clang"]), + ] + ), + .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), + .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), + .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), + .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), ] ) + +func getGtk4MinorVersion() -> Int? { + #if os(Windows) + return nil // Windows backend removed + #else + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["-c", "gtk4-launch --version"] + let pipe = Pipe() + process.standardOutput = pipe + + guard (try? process.run()) != nil, + let data = try? pipe.fileHandleForReading.readToEnd(), + case _ = process.waitUntilExit(), + let version = String(data: data, encoding: .utf8)?.split(separator: ".") + else { + print("Failed to get gtk version") + return nil + } + #endif + guard version.count >= 2, let minor = Int(version[1]) else { + print("Failed to get gtk version") + return nil + } + return minor +} From 6405aeed947121ae204243a60ba2c6081b5e1532 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:34:31 +1000 Subject: [PATCH 10/57] Update Package.swift --- Package.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Package.swift b/Package.swift index 8144de5e5b..acb40df7d9 100644 --- a/Package.swift +++ b/Package.swift @@ -70,9 +70,6 @@ let package = Package( targets: [ .target( name: "SwiftCrossUI", - dependencies: [ - .product(name: "ImageFormats", package: "swift-image-formats"), - ], exclude: [ "Builders/ViewBuilder.swift.gyb", "Builders/SceneBuilder.swift.gyb", From bc20086460d22fafdb5f0b431213f6c9dfd34f88 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:36:31 +1000 Subject: [PATCH 11/57] Update Image.swift --- Sources/SwiftCrossUI/Views/Image.swift | 58 +++++++++++--------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Image.swift b/Sources/SwiftCrossUI/Views/Image.swift index 2f3be3e6f8..85a1dd81d0 100644 --- a/Sources/SwiftCrossUI/Views/Image.swift +++ b/Sources/SwiftCrossUI/Views/Image.swift @@ -1,5 +1,4 @@ import Foundation -import ImageFormats /// A view that displays an image. public struct Image: Sendable { @@ -8,7 +7,7 @@ public struct Image: Sendable { enum Source: Equatable { case url(URL, useFileExtension: Bool) - case image(ImageFormats.Image) + case rawData(Data, width: Int, height: Int) } /// Displays an image file. `png`, `jpg`, and `webp` are supported. @@ -20,10 +19,13 @@ public struct Image: Sendable { source = .url(url, useFileExtension: useFileExtension) } - /// Displays an image from raw pixel data. - /// - Parameter image: The image data to display. - public init(_ image: ImageFormats.Image) { - source = .image(image) + /// Displays an image from raw RGBA pixel data. + /// - Parameters: + /// - data: Raw RGBA bytes. + /// - width: Width of the image in pixels. + /// - height: Height of the image in pixels. + public init(data: Data, width: Int, height: Int) { + source = .rawData(data, width: width, height: height) } /// Makes the image resize to fit the available space. @@ -74,44 +76,34 @@ extension Image: TypeSafeView { backend: Backend, dryRun: Bool ) -> ViewUpdateResult { - let image: ImageFormats.Image? + var imageData: (data: Data, width: Int, height: Int)? if source != children.cachedImageSource { switch source { - case .url(let url, let useFileExtension): + case .url(let url, _): if let data = try? Data(contentsOf: url) { - let bytes = Array(data) - if useFileExtension { - image = try? ImageFormats.Image.load( - from: bytes, - usingFileExtension: url.pathExtension - ) - } else { - image = try? ImageFormats.Image.load(from: bytes) - } - } else { - image = nil + // Backend should decode raw image bytes into RGBA + imageData = (data: data, width: 0, height: 0) } - case .image(let sourceImage): - image = sourceImage + case .rawData(let data, let width, let height): + imageData = (data: data, width: width, height: height) } - children.cachedImageSource = source - children.cachedImage = image + children.cachedImageData = imageData children.imageChanged = true } else { - image = children.cachedImage + imageData = children.cachedImageData } - let idealSize = SIMD2(image?.width ?? 0, image?.height ?? 0) + let idealSize = SIMD2(imageData?.width ?? 0, imageData?.height ?? 0) let size: ViewSize if isResizable { size = ViewSize( - size: image == nil ? .zero : proposedSize, + size: imageData == nil ? .zero : proposedSize, idealSize: idealSize, minimumWidth: 0, minimumHeight: 0, - maximumWidth: image == nil ? 0 : nil, - maximumHeight: image == nil ? 0 : nil + maximumWidth: imageData == nil ? 0 : nil, + maximumHeight: imageData == nil ? 0 : nil ) } else { size = ViewSize(fixedSize: idealSize) @@ -124,12 +116,12 @@ extension Image: TypeSafeView { || (backend.requiresImageUpdateOnScaleFactorChange && children.lastScaleFactor != environment.windowScaleFactor)) { - if let image { + if let imageData { backend.updateImageView( children.imageWidget.into(), - rgbaData: image.bytes, - width: image.width, - height: image.height, + rgbaData: imageData.data, + width: imageData.width, + height: imageData.height, targetWidth: size.size.x, targetHeight: size.size.y, dataHasChanged: children.imageChanged, @@ -161,7 +153,7 @@ extension Image: TypeSafeView { class _ImageChildren: ViewGraphNodeChildren { var cachedImageSource: Image.Source? = nil - var cachedImage: ImageFormats.Image? = nil + var cachedImageData: (data: Data, width: Int, height: Int)? = nil var cachedImageDisplaySize: SIMD2 = .zero var container: AnyWidget var imageWidget: AnyWidget From 91c6656590fcbf6b83ef685ecfa6bec9504947b5 Mon Sep 17 00:00:00 2001 From: JWI Date: Mon, 29 Sep 2025 18:44:29 +1000 Subject: [PATCH 12/57] Replace package access with internal --- .../SwiftCrossUI/Environment/EnvironmentValues.swift | 6 +++--- Sources/SwiftCrossUI/Environment/ListStyle.swift | 2 +- .../SwiftCrossUI/SwiftCrossUI.docc/AppKitBackend.md | 6 +++--- .../SwiftCrossUI/SwiftCrossUI.docc/Custom backends.md | 2 +- .../SwiftCrossUI/SwiftCrossUI.docc/DefaultBackend.md | 8 ++++---- Sources/SwiftCrossUI/SwiftCrossUI.docc/Gtk3Backend.md | 6 +++--- Sources/SwiftCrossUI/SwiftCrossUI.docc/GtkBackend.md | 10 +++++----- Sources/SwiftCrossUI/SwiftCrossUI.docc/UIKitBackend.md | 6 +++--- Sources/SwiftCrossUI/SwiftCrossUI.docc/WinUIBackend.md | 6 +++--- Sources/SwiftCrossUI/Values/ColorScheme.swift | 2 +- Sources/SwiftCrossUI/Values/DeviceClass.swift | 4 ++-- Sources/SwiftCrossUI/Values/Font.swift | 6 +++--- Sources/SwiftCrossUI/Views/Button.swift | 4 ++-- .../Views/Modifiers/EnvironmentModifier.swift | 10 +++++----- .../Modifiers/Handlers/OnTapGestureModifier.swift | 4 ++-- Sources/SwiftCrossUI/Views/Spacer.swift | 2 +- Sources/SwiftCrossUI/Views/Toggle.swift | 4 ++-- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index 475828ca63..7af864022f 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -40,7 +40,7 @@ public struct EnvironmentValues { /// helper method for our own backends. We haven't made this public because /// it would be weird to have two pretty equivalent ways of resolving fonts. @MainActor - package var resolvedFont: Font.Resolved { + public var resolvedFont: Font.Resolved { font.resolve(in: fontResolutionContext) } @@ -90,7 +90,7 @@ public struct EnvironmentValues { var onResize: @MainActor (_ newSize: ViewSize) -> Void /// The style of list to use. - package var listStyle: ListStyle + public var listStyle: ListStyle /// The style of toggle to use. public var toggleStyle: ToggleStyle @@ -121,7 +121,7 @@ public struct EnvironmentValues { /// The backend's representation of the window that the current view is /// in, if any. This is a very internal detail that should never get /// exposed to users. - package var window: Any? + public var window: Any? /// The backend in use. Mustn't change throughout the app's lifecycle. let backend: any AppBackend diff --git a/Sources/SwiftCrossUI/Environment/ListStyle.swift b/Sources/SwiftCrossUI/Environment/ListStyle.swift index 27042aa177..3b2e1f10cd 100644 --- a/Sources/SwiftCrossUI/Environment/ListStyle.swift +++ b/Sources/SwiftCrossUI/Environment/ListStyle.swift @@ -1,4 +1,4 @@ -package enum ListStyle { +public enum ListStyle { case `default` case sidebar } diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/AppKitBackend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/AppKitBackend.md index 814920e148..b56282f42a 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/AppKitBackend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/AppKitBackend.md @@ -11,15 +11,15 @@ SwiftCrossUI's native macOS backend built on top of AppKit. ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "AppKitBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "AppKitBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Custom backends.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Custom backends.md index c414e4be06..846b4388de 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Custom backends.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Custom backends.md @@ -3,7 +3,7 @@ ## Overview With being open and extensible as a core goal, SwiftCrossUI allows custom -backends to be implemented in third-party packages. +backends to be implemented in third-party publics. 'Simply' implement the ``AppBackend`` protocol and you're good to go! diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/DefaultBackend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/DefaultBackend.md index f0ef93357a..d3e7b337d4 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/DefaultBackend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/DefaultBackend.md @@ -8,22 +8,22 @@ The beauty of SwiftCrossUI is that you can write your app once and have it look > Tip: If you're using DefaultBackend, you can override the underlying backend during compilation by setting the `SCUI_DEFAULT_BACKEND` environment variable to the name of the desired backend. This is useful when you e.g. want to test the Gtk version of your app while using a Mac. Note that this only works for built-in backends and still requires the chosen backend to be compatible with your machine. -> Warning: When using `SCUI_DEFAULT_BACKEND` to switch underlying backends, you may encounter some linker-related missing symbol errors. These are caused by a SwiftPM bug and usually disappear if you run `swift package clean` before attempting to build your app again. +> Warning: When using `SCUI_DEFAULT_BACKEND` to switch underlying backends, you may encounter some linker-related missing symbol errors. These are caused by a SwiftPM bug and usually disappear if you run `swift public clean` before attempting to build your app again. ## Usage ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "DefaultBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "DefaultBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Gtk3Backend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Gtk3Backend.md index dc0703cffc..6c218dc7f9 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Gtk3Backend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Gtk3Backend.md @@ -43,15 +43,15 @@ If you try this on Windows open a GitHub issue (even if it works without changes ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "GtkBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "GtkBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/GtkBackend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/GtkBackend.md index c9bbbbbb5f..a94fc6d37f 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/GtkBackend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/GtkBackend.md @@ -99,12 +99,12 @@ First `vcpkg.json` at the root of your project and make sure that it includes th "dependencies": ["gtk"] } ``` -Figure 7: *an example `vcpkg.json` package manifest* +Figure 7: *an example `vcpkg.json` public manifest* ```sh C:\vcpkg\vcpkg.exe install --triplet x64-windows ``` -Figure 8: *install the dependencies listed in your package manifest* +Figure 8: *install the dependencies listed in your public manifest* > Warning: Replace the triplet with `arm64-windows` if you're on ARM64 @@ -113,15 +113,15 @@ Figure 8: *install the dependencies listed in your package manifest* ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "GtkBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "GtkBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/UIKitBackend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/UIKitBackend.md index d882599c33..6efd602d27 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/UIKitBackend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/UIKitBackend.md @@ -11,15 +11,15 @@ SwiftCrossUI's native iOS and tvOS backend built on top of UIKit. ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "UIKitBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "UIKitBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/WinUIBackend.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/WinUIBackend.md index 541f65f995..dcca4d6f3a 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/WinUIBackend.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/WinUIBackend.md @@ -21,15 +21,15 @@ Before you can use WinUIBackend you must install two dependencies; the former is ```swift ... -let package = Package( +let public = Package( ... targets: [ ... .executableTarget( name: "YourApp", dependencies: [ - .product(name: "SwiftCrossUI", package: "swift-cross-ui"), - .product(name: "WinUIBackend", package: "swift-cross-ui"), + .product(name: "SwiftCrossUI", public: "swift-cross-ui"), + .product(name: "WinUIBackend", public: "swift-cross-ui"), ] ) ... diff --git a/Sources/SwiftCrossUI/Values/ColorScheme.swift b/Sources/SwiftCrossUI/Values/ColorScheme.swift index d2fd897484..8818d9a8d5 100644 --- a/Sources/SwiftCrossUI/Values/ColorScheme.swift +++ b/Sources/SwiftCrossUI/Values/ColorScheme.swift @@ -2,7 +2,7 @@ public enum ColorScheme: Sendable { case light case dark - package var opposite: ColorScheme { + public var opposite: ColorScheme { switch self { case .light: .dark case .dark: .light diff --git a/Sources/SwiftCrossUI/Values/DeviceClass.swift b/Sources/SwiftCrossUI/Values/DeviceClass.swift index 44ee850584..4b9b30b6c8 100644 --- a/Sources/SwiftCrossUI/Values/DeviceClass.swift +++ b/Sources/SwiftCrossUI/Values/DeviceClass.swift @@ -1,14 +1,14 @@ /// A class of devices. Used to determine adaptive sizing behaviour such as /// the sizes of the various dynamic ``Font/TextStyle``s. public struct DeviceClass: Hashable, Sendable { - package enum Kind { + public enum Kind { case desktop case phone case tablet case tv } - package var kind: Kind + public var kind: Kind /// The device class for laptops and desktops. public static let desktop = Self(kind: .desktop) diff --git a/Sources/SwiftCrossUI/Values/Font.swift b/Sources/SwiftCrossUI/Values/Font.swift index b9187f3d98..ce9e6ed33e 100644 --- a/Sources/SwiftCrossUI/Values/Font.swift +++ b/Sources/SwiftCrossUI/Values/Font.swift @@ -212,11 +212,11 @@ public struct Font: Hashable, Sendable { public struct Resolved: Hashable, Sendable { public struct Identifier: Hashable, Sendable { - package var kind: Kind + public var kind: Kind public static let system = Self(kind: .system) - package enum Kind: Hashable { + public enum Kind: Hashable { case system } } @@ -236,7 +236,7 @@ public struct Font: Hashable, Sendable { } @MainActor - package func resolve(in context: Context) -> Resolved { + public func resolve(in context: Context) -> Resolved { let emphasizedWeight: Weight var resolved: Resolved switch kind { diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index 739d1ba407..e3dec57c22 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -1,9 +1,9 @@ /// A control that initiates an action. public struct Button: Sendable { /// The label to show on the button. - package var label: String + public var label: String /// The action to be performed when the button is clicked. - package var action: @MainActor @Sendable () -> Void + public var action: @MainActor @Sendable () -> Void /// The button's forced width if provided. var width: Int? diff --git a/Sources/SwiftCrossUI/Views/Modifiers/EnvironmentModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/EnvironmentModifier.swift index 801354c213..8df5f8f0e7 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/EnvironmentModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/EnvironmentModifier.swift @@ -1,13 +1,13 @@ -package struct EnvironmentModifier: View { - package var body: TupleView1 +public struct EnvironmentModifier: View { + public var body: TupleView1 var modification: (EnvironmentValues) -> EnvironmentValues - package init(_ child: Child, modification: @escaping (EnvironmentValues) -> EnvironmentValues) { + public init(_ child: Child, modification: @escaping (EnvironmentValues) -> EnvironmentValues) { self.body = TupleView1(child) self.modification = modification } - package func children( + public func children( backend: Backend, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues @@ -19,7 +19,7 @@ package struct EnvironmentModifier: View { ) } - package func update( + public func update( _ widget: Backend.Widget, children: any ViewGraphNodeChildren, proposedSize: SIMD2, diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Handlers/OnTapGestureModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Handlers/OnTapGestureModifier.swift index b8688eabbd..60e494ac4f 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Handlers/OnTapGestureModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Handlers/OnTapGestureModifier.swift @@ -1,5 +1,5 @@ public struct TapGesture: Sendable, Hashable { - package var kind: TapGestureKind + public var kind: TapGestureKind /// The idiomatic "primary" interaction for the device, such as a left-click with the mouse /// or normal tap on a touch screen. @@ -11,7 +11,7 @@ public struct TapGesture: Sendable, Hashable { /// ``secondary`` on some backends, particularly on mobile devices. public static let longPress = TapGesture(kind: .longPress) - package enum TapGestureKind { + public enum TapGestureKind { case primary, secondary, longPress } } diff --git a/Sources/SwiftCrossUI/Views/Spacer.swift b/Sources/SwiftCrossUI/Views/Spacer.swift index 913b3b6fa4..68f07a753d 100644 --- a/Sources/SwiftCrossUI/Views/Spacer.swift +++ b/Sources/SwiftCrossUI/Views/Spacer.swift @@ -3,7 +3,7 @@ public struct Spacer: ElementaryView, View { /// The minimum length this spacer can be shrunk to, along the axis of /// expansion. - package var minLength: Int? + public var minLength: Int? /// Creates a spacer with a given minimum length along its axis or axes /// of expansion. diff --git a/Sources/SwiftCrossUI/Views/Toggle.swift b/Sources/SwiftCrossUI/Views/Toggle.swift index dc787811ea..bcf6397995 100644 --- a/Sources/SwiftCrossUI/Views/Toggle.swift +++ b/Sources/SwiftCrossUI/Views/Toggle.swift @@ -40,7 +40,7 @@ public struct Toggle: View { /// A style of toggle. public struct ToggleStyle: Sendable { - package var style: Style + public var style: Style /// A toggle switch. public static let `switch` = Self(style: .switch) @@ -50,7 +50,7 @@ public struct ToggleStyle: Sendable { /// A checkbox. public static let checkbox = Self(style: .checkbox) - package enum Style { + public enum Style { case `switch` case button case checkbox From 404210e748a4fa345f726391913ccdb98a207c86 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:47:28 +1000 Subject: [PATCH 13/57] Update Image.swift --- Sources/SwiftCrossUI/Views/Image.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Views/Image.swift b/Sources/SwiftCrossUI/Views/Image.swift index 85a1dd81d0..a0fcc6762d 100644 --- a/Sources/SwiftCrossUI/Views/Image.swift +++ b/Sources/SwiftCrossUI/Views/Image.swift @@ -119,7 +119,7 @@ extension Image: TypeSafeView { if let imageData { backend.updateImageView( children.imageWidget.into(), - rgbaData: imageData.data, + rgbaData: [UInt8](imageData.data), width: imageData.width, height: imageData.height, targetWidth: size.size.x, From b37347694a97d3b315c131c25a9a7f2b27176b1a Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:48:41 +1000 Subject: [PATCH 14/57] Update UIKitBackend.swift --- Sources/UIKitBackend/UIKitBackend.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index f7580a6a06..6fd8e2f11b 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -27,7 +27,7 @@ public final class UIKitBackend: AppBackend { switch UIDevice.current.userInterfaceIdiom { case .phone: .phone - case .pad, .vision: + case .pad: .tablet case .tv: .tv From d6050bd0d0212da6f15502cc1fbeab0124ce5fa8 Mon Sep 17 00:00:00 2001 From: J_W_I_ <64947174+JWIMaster@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:30:29 +1000 Subject: [PATCH 15/57] Update UIKitBackend+Control.swift --- Sources/UIKitBackend/UIKitBackend+Control.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 9179075d20..461bcfc659 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -354,20 +354,6 @@ extension UIKitBackend { textEditorWidget.child.alwaysBounceVertical = environment.scrollDismissesKeyboardMode != .never - textEditorWidget.child.keyboardDismissMode = - switch environment.scrollDismissesKeyboardMode { - case .automatic: - textEditorWidget.child.inputAccessoryView == nil - ? .interactive : .interactiveWithAccessory - case .immediately: - textEditorWidget.child.inputAccessoryView == nil - ? .onDrag : .onDragWithAccessory - case .interactively: - textEditorWidget.child.inputAccessoryView == nil - ? .interactive : .interactiveWithAccessory - case .never: - .none - } #endif } From d92f13b6c3a54d3024ac26e138d3e95ce9ff59e7 Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 15:24:49 +1000 Subject: [PATCH 16/57] Added some compatibility checks and notes: Check marked things --- Package.resolved | 115 +----------------- Package.swift | 6 +- .../Modifiers/Lifecycle/TaskModifier.swift | 3 + .../ColorScheme+UIUserInterfaceStyle.swift | 1 + Sources/UIKitBackend/Font+UIFont.swift | 5 + Sources/UIKitBackend/UIKitBackend+Menu.swift | 1 + .../UIKitBackend/UIKitBackend+WebView.swift | 2 + .../UIKitBackend/UIKitBackend+Window.swift | 6 +- Sources/UIKitBackend/UIKitBackend.swift | 7 +- 9 files changed, 30 insertions(+), 116 deletions(-) diff --git a/Package.resolved b/Package.resolved index 18390df54a..36acd2ad16 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,49 +1,12 @@ { - "originHash" : "589b3dd67c6c4bf002ac0e661cdc5f048304c975897d3542f1623910c0b856d2", "pins" : [ - { - "identity" : "jpeg", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/jpeg", - "state" : { - "revision" : "a27e47f49479993b2541bc5f5c95d9354ed567a0", - "version" : "1.0.2" - } - }, - { - "identity" : "libpng", - "kind" : "remoteSourceControl", - "location" : "https://github.com/the-swift-collective/libpng", - "state" : { - "revision" : "0eff23aca92a086b7892831f5cb4f58e15be9449", - "version" : "1.6.45" - } - }, - { - "identity" : "libwebp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/the-swift-collective/libwebp", - "state" : { - "revision" : "5f745a17b9a5c2a4283f17c2cde4517610ab5f99", - "version" : "1.4.1" - } - }, - { - "identity" : "swift-cwinrt", - "kind" : "remoteSourceControl", - "location" : "https://github.com/thebrowsercompany/swift-cwinrt", - "state" : { - "branch" : "main", - "revision" : "3e3d5a0270ebe8fb0bc738d24d4786a6cd0621eb" - } - }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-docc-plugin", "state" : { - "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", - "version" : "1.4.3" + "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", + "version" : "1.4.5" } }, { @@ -55,72 +18,13 @@ "version" : "1.0.0" } }, - { - "identity" : "swift-image-formats", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-image-formats", - "state" : { - "revision" : "2e5dc1ead747afab9fd517d81316d961969e3610", - "version" : "0.3.3" - } - }, - { - "identity" : "swift-macro-toolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-macro-toolkit", - "state" : { - "revision" : "e706aa98bc28f82677923f7b8f560bba6f90fac2", - "version" : "0.6.0" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "swift-uwp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-uwp", - "state" : { - "revision" : "c9d3fc079aaaa5113cde9a0132278fb83e808599" - } - }, - { - "identity" : "swift-webview2core", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-webview2core", - "state" : { - "revision" : "9afd97424f844478914ca4512c8ca0a2d3a2bb67" - } - }, - { - "identity" : "swift-windowsappsdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-windowsappsdk", - "state" : { - "revision" : "ed938db0b9790b36391dc91b20cee81f2410309f" - } - }, - { - "identity" : "swift-windowsfoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/thebrowsercompany/swift-windowsfoundation", - "state" : { - "branch" : "main", - "revision" : "dbe14563b6bb0eb9c761d8aff7f465afddf185f9" - } - }, - { - "identity" : "swift-winui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-winui", - "state" : { - "revision" : "927e2c46430cfb1b6c195590b9e65a30a8fd98a2" + "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", + "version" : "0.50700.1" } }, { @@ -131,16 +35,7 @@ "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", "version" : "0.17.1" } - }, - { - "identity" : "zlib", - "kind" : "remoteSourceControl", - "location" : "https://github.com/the-swift-collective/zlib.git", - "state" : { - "revision" : "f1d153b90420f9fcc6ef916cd67ea96f0e68d137", - "version" : "1.3.2" - } } ], - "version" : 3 + "version" : 2 } diff --git a/Package.swift b/Package.swift index acb40df7d9..f3ad1dbd9d 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS(.v13), + .iOS(.v8), .tvOS(.v13), .macCatalyst(.v13) ], @@ -63,9 +63,9 @@ let package = Package( .executable(name: "GtkExample", targets: ["GtkExample"]), ], dependencies: [ - .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1"), + .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.10.1"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "500.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "0.0.0"), ], targets: [ .target( diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift index 62726cf32a..ae5210bec3 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift @@ -1,3 +1,6 @@ +//MARK: Is this needed? This seems to be the worst offender in terms of compatibility. + + extension View { /// Starts a task before a view appears (but after ``View/body`` has been /// accessed), and cancels the task when the view disappears. Additionally, diff --git a/Sources/UIKitBackend/ColorScheme+UIUserInterfaceStyle.swift b/Sources/UIKitBackend/ColorScheme+UIUserInterfaceStyle.swift index d30cc2b0d9..dfd49e6e38 100644 --- a/Sources/UIKitBackend/ColorScheme+UIUserInterfaceStyle.swift +++ b/Sources/UIKitBackend/ColorScheme+UIUserInterfaceStyle.swift @@ -2,6 +2,7 @@ import SwiftCrossUI import UIKit extension ColorScheme { + @available(iOS 12, *) var userInterfaceStyle: UIUserInterfaceStyle { switch self { case .light: diff --git a/Sources/UIKitBackend/Font+UIFont.swift b/Sources/UIKitBackend/Font+UIFont.swift index 363e86b3e7..92e83798b9 100644 --- a/Sources/UIKitBackend/Font+UIFont.swift +++ b/Sources/UIKitBackend/Font+UIFont.swift @@ -29,8 +29,13 @@ extension Font.Resolved { } switch design { + //MARK: Sketchy asf case .monospaced: + if #available(iOS 13.0, *) { uiFont = .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) + } else { + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) + } case .default: uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) } diff --git a/Sources/UIKitBackend/UIKitBackend+Menu.swift b/Sources/UIKitBackend/UIKitBackend+Menu.swift index efe33affa9..93996bc8ae 100644 --- a/Sources/UIKitBackend/UIKitBackend+Menu.swift +++ b/Sources/UIKitBackend/UIKitBackend+Menu.swift @@ -1,6 +1,7 @@ import SwiftCrossUI import UIKit +@available(iOS 13, *) extension UIKitBackend { public final class Menu { var uiMenu: UIMenu? diff --git a/Sources/UIKitBackend/UIKitBackend+WebView.swift b/Sources/UIKitBackend/UIKitBackend+WebView.swift index b4b74c062f..922484d2ea 100644 --- a/Sources/UIKitBackend/UIKitBackend+WebView.swift +++ b/Sources/UIKitBackend/UIKitBackend+WebView.swift @@ -1,6 +1,7 @@ import SwiftCrossUI import WebKit +@available(iOS 8, *) extension UIKitBackend { public func createWebView() -> Widget { WebViewWidget() @@ -23,6 +24,7 @@ extension UIKitBackend { } /// A wrapper for WKWebView. Acts as the web view's delegate as well. +@available(iOS 8, *) final class WebViewWidget: WrapperWidget, WKNavigationDelegate { var onNavigate: ((URL) -> Void)? diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 46aa666ee7..cc44fd043c 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -146,7 +146,9 @@ extension UIKitBackend { public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2) { // if windowScene is nil, either the window isn't shown or it must be fullscreen // if sizeRestrictions is nil, the device doesn't support setting a minimum window size - window.windowScene?.sizeRestrictions?.minimumSize = CGSize( - width: CGFloat(minimumSize.x), height: CGFloat(minimumSize.y)) + if #available(iOS 13, *) { + window.windowScene?.sizeRestrictions?.minimumSize = CGSize( + width: CGFloat(minimumSize.x), height: CGFloat(minimumSize.y)) + } } } diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index 6fd8e2f11b..09a2f373a0 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -82,7 +82,9 @@ public final class UIKitBackend: AppBackend { Self.onReceiveURL = action } - + + + // MARK: TODO - Fixup this to be compatible with iOS 6, somehow public func computeRootEnvironment(defaultEnvironment: EnvironmentValues) -> EnvironmentValues { var environment = defaultEnvironment @@ -238,6 +240,7 @@ open class ApplicationDelegate: UIResponder, UIApplicationDelegate { /// you also need to control the menus' identifiers. /// /// This method is only used on Mac Catalyst. + @available(iOS 13, *) open func mapMenuIdentifier(_ label: String) -> UIMenu.Identifier { switch label { case "File": .file @@ -259,6 +262,7 @@ open class ApplicationDelegate: UIResponder, UIApplicationDelegate { /// When targeting Mac Catalyst, you should call `super.buildMenu(with: builder)` at some /// point in your implementation. If you do not, then calls to /// ``SwiftCrossUI/Scene/commands(_:)`` will have no effect. + @available(iOS 13, *) open override func buildMenu(with builder: any UIMenuBuilder) { guard #available(tvOS 14, *), builder.system == .main @@ -282,6 +286,7 @@ open class ApplicationDelegate: UIResponder, UIApplicationDelegate { /// /// SwiftCrossUI apps do not have to be scene-based. If you are writing a scene-based app, /// derive your scene delegate from this class. +@available(iOS 13, *) open class SceneDelegate: UIResponder, UIWindowSceneDelegate { public var window: UIWindow? { willSet { From 9943597ebb9a68041cc2380948ef91ebfad07527 Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:20:04 +1000 Subject: [PATCH 17/57] testing some stuff --- Package.resolved | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 36acd2ad16..0000000000 --- a/Package.resolved +++ /dev/null @@ -1,41 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-plugin", - "state" : { - "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", - "version" : "1.4.5" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", - "version" : "0.50700.1" - } - }, - { - "identity" : "xmlcoder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CoreOffice/XMLCoder", - "state" : { - "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", - "version" : "0.17.1" - } - } - ], - "version" : 2 -} From ea7830e835abeb9c34b43506c8ccc6308debb29e Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:20:15 +1000 Subject: [PATCH 18/57] whoops --- Package.resolved | 32 ++++++++++++++++++++++++++++++++ Package.swift | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000000..6051561377 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-docc-plugin", + "state" : { + "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", + "version" : "0.50700.1" + } + }, + { + "identity" : "xmlcoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CoreOffice/XMLCoder", + "state" : { + "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", + "version" : "0.17.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index f3ad1dbd9d..0602a4ab76 100644 --- a/Package.swift +++ b/Package.swift @@ -63,8 +63,8 @@ let package = Package( .executable(name: "GtkExample", targets: ["GtkExample"]), ], dependencies: [ - .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.10.1"), - .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.0.0"), + .package(url: "https://github.com/swiftlang/swift-docc-plugin", exact: "1.0.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "0.0.0"), ], targets: [ From ace15304790854ebb7976f389fcd09562ded1b22 Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:26:20 +1000 Subject: [PATCH 19/57] Marked TaskModifier as iOS 13+ --- .../SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift index ae5210bec3..68fe267792 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift @@ -1,6 +1,6 @@ //MARK: Is this needed? This seems to be the worst offender in terms of compatibility. - +@available(iOS 13, *) extension View { /// Starts a task before a view appears (but after ``View/body`` has been /// accessed), and cancels the task when the view disappears. Additionally, From 12ccc5501ce469cf7b49224ffbfd927349bab222 Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:43:55 +1000 Subject: [PATCH 20/57] Provide fallback for PresentAlertAction --- .../Actions/PresentAlertAction.swift | 53 ++++++++++++++++--- .../Lifecycle/OnDisappearModifier.swift | 12 ++++- .../Modifiers/Lifecycle/TaskModifier.swift | 2 + 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift index a045493766..cd7dab9d71 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift @@ -7,6 +7,8 @@ public struct PresentAlertAction { let environment: EnvironmentValues + // MARK: - iOS 13+ version with async/await + @available(iOS 13.0, *) @discardableResult public func callAsFunction( _ title: String, @@ -24,16 +26,15 @@ public struct PresentAlertAction { actionLabels: actions.map(\.label), environment: environment ) - let window: Backend.Window? = + + let window: Backend.Window? = { if let window = environment.window { - .some(window as! Backend.Window) - } else { - nil + return window as? Backend.Window } - backend.showAlert( - alert, - window: window - ) { actionIndex in + return nil + }() + + backend.showAlert(alert, window: window) { actionIndex in actions[actionIndex].action() continuation.resume(returning: actionIndex) } @@ -43,4 +44,40 @@ public struct PresentAlertAction { return await presentAlert(backend: environment.backend) } + + // MARK: - iOS 12 and below version with completion handler + @discardableResult + public func callAsFunction( + _ title: String, + @AlertActionsBuilder actions: () -> [AlertAction] = { [.ok] }, + completion: @escaping (Int) -> Void + ) { + let actions = actions() + + func presentAlert(backend: Backend, completion: @escaping (Int) -> Void) { + backend.runInMainThread { + let alert = backend.createAlert() + backend.updateAlert( + alert, + title: title, + actionLabels: actions.map(\.label), + environment: environment + ) + + let window: Backend.Window? = { + if let window = environment.window { + return window as? Backend.Window + } + return nil + }() + + backend.showAlert(alert, window: window) { actionIndex in + actions[actionIndex].action() + completion(actionIndex) + } + } + } + + presentAlert(backend: environment.backend, completion: completion) + } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift index 4e0a5f413c..9af2cbce98 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift @@ -1,3 +1,5 @@ +import Foundation + extension View { /// Adds an action to be performed after this view disappears. /// @@ -86,8 +88,14 @@ class OnDisappearModifierChildren: ViewGraphNodeChildren { } deinit { - Task { @MainActor [action] in - action() + if #available(iOS 13, *) { + Task { @MainActor [action] in + action() + } + } else { + DispatchQueue.main.async { + self.action() + } } } } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift index 68fe267792..f81934777a 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift @@ -37,6 +37,7 @@ extension View { } } +@available(iOS 13, *) struct TaskModifier { @State var task: Task<(), any Error>? = nil @@ -46,6 +47,7 @@ struct TaskModifier { var action: () async -> Void } +@available(iOS 13, *) extension TaskModifier: View { var body: some View { // Explicitly return to disable result builder (we don't want an extra From 4773c6093d869d47dadb938c8ea21257e643bb4d Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:47:27 +1000 Subject: [PATCH 21/57] Same for PresentFileSaveDialogAction --- .../Actions/PresentFileSaveDialogAction.swift | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift index 5905650011..bcc1fb428c 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift @@ -5,7 +5,8 @@ import Foundation public struct PresentFileSaveDialogAction: Sendable { let backend: any AppBackend let window: MainActorBox - + + @available(iOS 13, *) public func callAsFunction( title: String = "Save", message: String = "", @@ -53,4 +54,52 @@ public struct PresentFileSaveDialogAction: Sendable { return await chooseFile(backend: backend) } + + // MARK: - iOS 12 and below version with completion handler + public func callAsFunction( + title: String = "Save", + message: String = "", + defaultButtonLabel: String = "Save", + initialDirectory: URL? = nil, + showHiddenFiles: Bool = false, + nameFieldLabel: String? = nil, + defaultFileName: String? = nil, + completion: @escaping (URL?) -> Void + ) { + func chooseFile(backend: Backend, completion: @escaping (URL?) -> Void) { + backend.runInMainThread { + let window: Backend.Window? = { + if let window = self.window.value { + return window as? Backend.Window + } + return nil + }() + + backend.showSaveDialog( + fileDialogOptions: FileDialogOptions( + title: title, + defaultButtonLabel: defaultButtonLabel, + allowedContentTypes: [], + showHiddenFiles: showHiddenFiles, + allowOtherContentTypes: true, + initialDirectory: initialDirectory + ), + saveDialogOptions: SaveDialogOptions( + nameFieldLabel: nameFieldLabel, + defaultFileName: defaultFileName + ), + window: window + ) { result in + switch result { + case .success(let url): + completion(url) + case .cancelled: + completion(nil) + } + } + } + } + + chooseFile(backend: backend, completion: completion) + } } From 1c7a1f3bdc80e3936b04ec41f55f1112ba5e4c6e Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:50:19 +1000 Subject: [PATCH 22/57] Same for PresentSingleFileOpenDialogAction --- .../PresentSingleFileOpenDialogAction.swift | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift index 330bba3a47..fec874eaa3 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift @@ -3,10 +3,12 @@ import Foundation /// Presents an 'Open file' dialog fit for selecting a single file. Some /// backends only allow selecting either files or directories but not both /// in a single dialog. Returns `nil` if the user cancels the operation. -public struct PresentSingleFileOpenDialogAction: Sendable { +public struct PresentSingleFileOpenDialogAction { let backend: any AppBackend let window: MainActorBox + // MARK: - iOS 13+ async/await version + @available(iOS 13.0, *) public func callAsFunction( title: String = "Open", message: String = "", @@ -19,12 +21,12 @@ public struct PresentSingleFileOpenDialogAction: Sendable { func chooseFile(backend: Backend) async -> URL? { await withCheckedContinuation { continuation in backend.runInMainThread { - let window: Backend.Window? = + let window: Backend.Window? = { if let window = self.window.value { - .some(window as! Backend.Window) - } else { - nil + return window as? Backend.Window } + return nil + }() backend.showOpenDialog( fileDialogOptions: FileDialogOptions( @@ -43,8 +45,8 @@ public struct PresentSingleFileOpenDialogAction: Sendable { window: window ) { result in switch result { - case .success(let url): - continuation.resume(returning: url[0]) + case .success(let urls): + continuation.resume(returning: urls.first) case .cancelled: continuation.resume(returning: nil) } @@ -55,4 +57,53 @@ public struct PresentSingleFileOpenDialogAction: Sendable { return await chooseFile(backend: backend) } + + // MARK: - iOS 12 and below version with completion handler + public func callAsFunction( + title: String = "Open", + message: String = "", + defaultButtonLabel: String = "Open", + initialDirectory: URL? = nil, + showHiddenFiles: Bool = false, + allowSelectingFiles: Bool = true, + allowSelectingDirectories: Bool = false, + completion: @escaping (URL?) -> Void + ) { + func chooseFile(backend: Backend, completion: @escaping (URL?) -> Void) { + backend.runInMainThread { + let window: Backend.Window? = { + if let window = self.window.value { + return window as? Backend.Window + } + return nil + }() + + backend.showOpenDialog( + fileDialogOptions: FileDialogOptions( + title: title, + defaultButtonLabel: defaultButtonLabel, + allowedContentTypes: [], + showHiddenFiles: showHiddenFiles, + allowOtherContentTypes: true, + initialDirectory: initialDirectory + ), + openDialogOptions: OpenDialogOptions( + allowSelectingFiles: allowSelectingFiles, + allowSelectingDirectories: allowSelectingDirectories, + allowMultipleSelections: false + ), + window: window + ) { result in + switch result { + case .success(let urls): + completion(urls.first) + case .cancelled: + completion(nil) + } + } + } + } + + chooseFile(backend: backend, completion: completion) + } } From d4b9563dd1514bb76d33f627e74615acad048221 Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 16:53:42 +1000 Subject: [PATCH 23/57] Added a temporary shim for Identifiable type, will remove later --- Sources/SwiftCrossUI/Views/List.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/SwiftCrossUI/Views/List.swift b/Sources/SwiftCrossUI/Views/List.swift index 4283e5249e..cdfff42898 100644 --- a/Sources/SwiftCrossUI/Views/List.swift +++ b/Sources/SwiftCrossUI/Views/List.swift @@ -1,3 +1,10 @@ +//MARK: Will add to a shim library later, just here for testing now. +@available(iOS, introduced: 8.0, obsoleted: 13.0) +public protocol Identifiable { + associatedtype ID: Hashable + var id: ID { get } +} + public struct List: TypeSafeView, View { typealias Children = ListViewChildren> From 6b013f8cb37ac9eaede7c550a03082c56168ad7e Mon Sep 17 00:00:00 2001 From: JWI Date: Thu, 2 Oct 2025 22:05:38 +1000 Subject: [PATCH 24/57] Tonnes of changes for compatibility, changed version to 7.0, added my compat library --- Package.resolved | 9 ++ Package.swift | 8 +- Sources/UIKitBackend/Font+UIFont.swift | 108 +++++++++++------- Sources/UIKitBackend/UIColor+Color.swift | 23 +++- Sources/UIKitBackend/UIKitBackend+Alert.swift | 4 + .../UIKitBackend/UIKitBackend+Container.swift | 1 + .../UIKitBackend/UIKitBackend+Control.swift | 52 +++++++-- .../UIKitBackend/UIKitBackend+SplitView.swift | 16 ++- .../UIKitBackend/UIKitBackend+Window.swift | 1 + Sources/UIKitBackend/UIKitBackend.swift | 13 ++- 10 files changed, 170 insertions(+), 65 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6051561377..2977be4c08 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "version" : "0.50700.1" } }, + { + "identity" : "uikitcompatkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JWIMaster/UIKitCompatKit", + "state" : { + "branch" : "master", + "revision" : "1661baf96942b632f9e007b0c29e4453fe47d3f4" + } + }, { "identity" : "xmlcoder", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 0602a4ab76..f3be25be2b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS(.v8), + .iOS("7.0"), .tvOS(.v13), .macCatalyst(.v13) ], @@ -66,6 +66,7 @@ let package = Package( .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.0.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", exact: "1.0.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "0.0.0"), + .package(url: "https://github.com/JWIMaster/UIKitCompatKit", branch: "master"), ], targets: [ .target( @@ -120,7 +121,10 @@ let package = Package( .target(name: "Gtk3", dependencies: ["CGtk3", "Gtk3CustomWidgets"], exclude: ["LICENSE.md"], swiftSettings: gtkSwiftSettings), .executableTarget(name: "Gtk3Example", dependencies: ["Gtk3"], resources: [.copy("GTK.png")]), .target(name: "Gtk3CustomWidgets", dependencies: ["CGtk3"]), - .target(name: "UIKitBackend", dependencies: ["SwiftCrossUI"]), + .target(name: "UIKitBackend", dependencies: [ + "SwiftCrossUI", + .product(name: "UIKitCompatKit", package: "UIKitCompatKit"), + ]), ] ) diff --git a/Sources/UIKitBackend/Font+UIFont.swift b/Sources/UIKitBackend/Font+UIFont.swift index 92e83798b9..0d88b1a844 100644 --- a/Sources/UIKitBackend/Font+UIFont.swift +++ b/Sources/UIKitBackend/Font+UIFont.swift @@ -1,54 +1,78 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit extension Font.Resolved { var uiFont: UIFont { let uiFont: UIFont - switch identifier.kind { - case .system: - let weight: UIFont.Weight = - switch weight { - case .ultraLight: - .ultraLight - case .thin: - .thin - case .light: - .light - case .regular: - .regular - case .medium: - .medium - case .semibold: - .semibold - case .bold: - .bold - case .heavy: - .heavy - case .black: - .black + + if #available(iOS 8.2, *) { + switch identifier.kind { + case .system: + let weight: UIFont.Weight = + switch weight { + case .ultraLight: .ultraLight + case .thin: .thin + case .light: .light + case .regular: .regular + case .medium: .medium + case .semibold: .semibold + case .bold: .bold + case .heavy: .heavy + case .black: .black + } + + switch design { + //MARK: Sketchy asf + case .monospaced: + if #available(iOS 13.0, *) { + uiFont = .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) + } else { + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) + } + case .default: + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) } + } - switch design { - //MARK: Sketchy asf - case .monospaced: - if #available(iOS 13.0, *) { - uiFont = .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) - } else { - uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) - } - case .default: - uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) - } - } - - if isItalic { - let descriptor = uiFont.fontDescriptor.withSymbolicTraits(.traitItalic) - return UIFont( - descriptor: descriptor ?? uiFont.fontDescriptor, - size: CGFloat(pointSize) - ) + if isItalic { + let descriptor = uiFont.fontDescriptor.withSymbolicTraits(.traitItalic) + return UIFont( + descriptor: descriptor ?? uiFont.fontDescriptor, + size: CGFloat(pointSize) + ) + } else { + return uiFont + } } else { - return uiFont + switch identifier.kind { + case .system: + let weight: UIFontWeight = + switch weight { + case .ultraLight: .ultraLight + case .thin: .thin + case .light: .light + case .regular: .regular + case .medium: .medium + case .semibold: .semibold + case .bold: .bold + case .heavy: .heavy + case .black: .black + } + + switch design { + //MARK: Sketchy asf + case .monospaced: + if #available(iOS 13.0, *) { + uiFont = .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) + } else { + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) + } + case .default: + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) + } + return uiFont + } } } } diff --git a/Sources/UIKitBackend/UIColor+Color.swift b/Sources/UIKitBackend/UIColor+Color.swift index 6d7d468c75..0c234fd383 100644 --- a/Sources/UIKitBackend/UIColor+Color.swift +++ b/Sources/UIKitBackend/UIColor+Color.swift @@ -1,5 +1,6 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit extension UIColor { convenience init(color: Color) { @@ -29,11 +30,21 @@ extension Color { } var cgColor: CGColor { - CGColor( - red: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) + if #available(iOS 13.0, *) { + CGColor( + red: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) + } else { + //MARK: this won't work right now, get back to me on it. + CGColorShim( + red: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) as! CGColor + } } } diff --git a/Sources/UIKitBackend/UIKitBackend+Alert.swift b/Sources/UIKitBackend/UIKitBackend+Alert.swift index cc60fceb87..0d34d7e677 100644 --- a/Sources/UIKitBackend/UIKitBackend+Alert.swift +++ b/Sources/UIKitBackend/UIKitBackend+Alert.swift @@ -1,6 +1,10 @@ import SwiftCrossUI import UIKit + + +//MARK: Might come back and make a shim for this :P +@available(iOS 8.0, *) extension UIKitBackend { public final class Alert { let controller: UIAlertController diff --git a/Sources/UIKitBackend/UIKitBackend+Container.swift b/Sources/UIKitBackend/UIKitBackend+Container.swift index f23ee39a7d..bb7cae5052 100644 --- a/Sources/UIKitBackend/UIKitBackend+Container.swift +++ b/Sources/UIKitBackend/UIKitBackend+Container.swift @@ -1,5 +1,6 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit final class ScrollWidget: ContainerWidget { private var scrollView = UIScrollView() diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 461bcfc659..fc1dd69925 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -171,6 +171,7 @@ final class TappableWidget: ContainerWidget { } @available(tvOS, unavailable) +@available(iOS 13, *) final class HoverableWidget: ContainerWidget { private var hoverGestureRecognizer: UIHoverGestureRecognizer? @@ -242,6 +243,16 @@ extension UIKitBackend { // and all round looks quite sloppy. Therefore, it's safest to just // ignore foreground color for buttons on tvOS until we have a better // solution. + let foregroundColor: UIColor + if #available(iOS 13, *) { + foregroundColor = .link + } else if #available(iOS 7, *) { + // fallback color for older iOS versions + foregroundColor = UIColor.systemBlue + } else { + foregroundColor = UIColor.blue + } + #if os(tvOS) buttonWidget.child.setTitle(label, for: .normal) #else @@ -249,7 +260,7 @@ extension UIKitBackend { UIKitBackend.attributedString( text: label, environment: environment, - defaultForegroundColor: .link + defaultForegroundColor: foregroundColor ), for: .normal ) @@ -290,9 +301,20 @@ extension UIKitBackend { textFieldWidget.onChange = onChange textFieldWidget.onSubmit = onSubmit - let (keyboardType, contentType) = splitTextContentType(environment.textContentType) + let keyboardType: UIKeyboardType + let contentType: UITextContentType? + + if #available(iOS 10, *) { + (keyboardType, contentType) = splitTextContentType(environment.textContentType) + } else { + keyboardType = .default + contentType = nil + } + textFieldWidget.child.keyboardType = keyboardType - textFieldWidget.child.textContentType = contentType + if #available(iOS 10, *) { + textFieldWidget.child.textContentType = contentType + } #if os(iOS) if let updateToolbar = environment.updateToolbar { @@ -336,10 +358,23 @@ extension UIKitBackend { textEditorWidget.child.font = environment.resolvedFont.uiFont textEditorWidget.child.textColor = UIColor(color: environment.suggestedForegroundColor) textEditorWidget.onChange = onChange + + + let keyboardType: UIKeyboardType + let contentType: UITextContentType? + + if #available(iOS 10, *) { + (keyboardType, contentType) = splitTextContentType(environment.textContentType) + } else { + keyboardType = .default + contentType = nil + } - let (keyboardType, contentType) = splitTextContentType(environment.textContentType) textEditorWidget.child.keyboardType = keyboardType - textEditorWidget.child.textContentType = contentType + if #available(iOS 10, *) { + textEditorWidget.child.textContentType = contentType + } + #if os(iOS) if let updateToolbar = environment.updateToolbar { @@ -369,6 +404,7 @@ extension UIKitBackend { // Splits a SwiftCrossUI TextContentType into a UIKit keyboard type and // text content type. + @available(iOS 10, *) private func splitTextContentType( _ textContentType: TextContentType ) -> (UIKeyboardType, UITextContentType?) { @@ -433,11 +469,13 @@ extension UIKitBackend { wrapper.onLongPress = environment.isEnabled ? action : {} } } - + + @available(iOS 13, *) public func createHoverTarget(wrapping child: Widget) -> Widget { HoverableWidget(child: child) } - + + @available(iOS 13, *) public func updateHoverTarget( _ hoverTarget: any WidgetProtocol, environment: EnvironmentValues, diff --git a/Sources/UIKitBackend/UIKitBackend+SplitView.swift b/Sources/UIKitBackend/UIKitBackend+SplitView.swift index 7603abb524..42a461d074 100644 --- a/Sources/UIKitBackend/UIKitBackend+SplitView.swift +++ b/Sources/UIKitBackend/UIKitBackend+SplitView.swift @@ -1,4 +1,5 @@ import UIKit +import UIKitCompatKit #if os(iOS) final class SplitWidget: WrapperControllerWidget, @@ -16,9 +17,12 @@ import UIKit super.init(child: UISplitViewController()) child.delegate = self - - child.preferredDisplayMode = .oneBesideSecondary - child.preferredPrimaryColumnWidthFraction = 0.3 + + + if #available(iOS 8.0, *) { + child.preferredDisplayMode = .oneBesideSecondary + child.preferredPrimaryColumnWidthFraction = 0.3 + } child.viewControllers = [sidebarContainer, mainContainer] } @@ -71,12 +75,14 @@ import UIKit let splitWidget = splitView as! SplitWidget splitWidget.resizeHandler = action } - + + @available(iOS 8, *) public func sidebarWidth(ofSplitView splitView: Widget) -> Int { let splitWidget = splitView as! SplitWidget return Int(splitWidget.child.primaryColumnWidth.rounded(.toNearestOrEven)) } - + + @available(iOS 8, *) public func setSidebarWidthBounds( ofSplitView splitView: Widget, minimum minimumWidth: Int, diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index cc44fd043c..6cd87e28fa 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -1,4 +1,5 @@ import UIKit +import UIKitCompatKit final class RootViewController: UIViewController { unowned var backend: UIKitBackend diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index 09a2f373a0..823339665a 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -89,14 +89,20 @@ public final class UIKitBackend: AppBackend { var environment = defaultEnvironment environment.toggleStyle = .switch - - switch UITraitCollection.current.userInterfaceStyle { + + + if #available(iOS 13.0, *) { + switch UITraitCollection.current.userInterfaceStyle { case .light: environment.colorScheme = .light case .dark: environment.colorScheme = .dark default: break + } + } else { + //Default to light on all other platforms + environment.colorScheme = .light } return environment @@ -127,7 +133,8 @@ public final class UIKitBackend: AppBackend { public func show(widget: Widget) { } - + + @available(iOS 13, *) public func openExternalURL(_ url: URL) throws { UIApplication.shared.open(url) } From 26fd63ce02863145b2c40ed97a01001ae655894c Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 08:54:51 +1000 Subject: [PATCH 25/57] added some more compatibility stuff --- Package.resolved | 2 +- Sources/UIKitBackend/UIKitBackend+Passive.swift | 12 +++++++++--- Sources/UIKitBackend/UIKitBackend+Window.swift | 16 ++++++++++++---- .../UIViewControllerRepresentable.swift | 1 + Sources/UIKitBackend/UIViewRepresentable.swift | 1 + Sources/UIKitBackend/Widget.swift | 1 + 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2977be4c08..46defca676 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,7 +24,7 @@ "location" : "https://github.com/JWIMaster/UIKitCompatKit", "state" : { "branch" : "master", - "revision" : "1661baf96942b632f9e007b0c29e4453fe47d3f4" + "revision" : "0587a8892c66b3ac321f1847581a6b4752f5655a" } }, { diff --git a/Sources/UIKitBackend/UIKitBackend+Passive.swift b/Sources/UIKitBackend/UIKitBackend+Passive.swift index 235e7b893c..a753531a1a 100644 --- a/Sources/UIKitBackend/UIKitBackend+Passive.swift +++ b/Sources/UIKitBackend/UIKitBackend+Passive.swift @@ -1,5 +1,6 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit extension UIKitBackend { static func attributedString( @@ -15,7 +16,8 @@ extension UIKitBackend { case .leading: .natural case .trailing: - UITraitCollection.current.layoutDirection == .rightToLeft ? .left : .right + //UITraitCollection.current.layoutDirection == .rightToLeft ? .left : .right + .center //I don't know how to fix this right now } paragraphStyle.lineBreakMode = .byWordWrapping @@ -47,7 +49,11 @@ extension UIKitBackend { environment: EnvironmentValues ) { let wrapper = textView as! WrapperWidget - wrapper.child.overrideUserInterfaceStyle = environment.colorScheme.userInterfaceStyle + + if #available(iOS 13.0, *) { + wrapper.child.overrideUserInterfaceStyle = environment.colorScheme.userInterfaceStyle + } + wrapper.child.attributedText = UIKitBackend.attributedString( text: content, environment: environment @@ -99,7 +105,7 @@ extension UIKitBackend { bytesPerRow: width * 4, size: CGSize(width: CGFloat(width), height: CGFloat(height)), format: .RGBA8, - colorSpace: .init(name: CGColorSpace.sRGB) + colorSpace: .init(name: "sRGB" as CFString) //temporary modification lol ) wrapper.child.image = .init(ciImage: ciImage) } diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 6cd87e28fa..2ba687268a 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -1,5 +1,5 @@ import UIKit -import UIKitCompatKit +import UIKitCompatKit final class RootViewController: UIViewController { unowned var backend: UIKitBackend @@ -21,7 +21,9 @@ final class RootViewController: UIViewController { self.backend = backend super.init(nibName: nil, bundle: nil) } - + + + @available(iOS 8.0, *) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) backend.onTraitCollectionChange?() @@ -35,11 +37,17 @@ final class RootViewController: UIViewController { override func loadView() { super.loadView() - if traitCollection.userInterfaceStyle != .dark { + if #available(iOS 12, *) { + if traitCollection.userInterfaceStyle != .dark { + view.backgroundColor = .white + } + } else { view.backgroundColor = .white } } - + + + @available(iOS 8.0, *) override func viewWillTransition( to size: CGSize, with coordinator: any UIViewControllerTransitionCoordinator ) { diff --git a/Sources/UIKitBackend/UIViewControllerRepresentable.swift b/Sources/UIKitBackend/UIViewControllerRepresentable.swift index cfd73e8437..679aadf84f 100644 --- a/Sources/UIKitBackend/UIViewControllerRepresentable.swift +++ b/Sources/UIKitBackend/UIViewControllerRepresentable.swift @@ -1,5 +1,6 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit public struct UIViewControllerRepresentableContext { public let coordinator: Coordinator diff --git a/Sources/UIKitBackend/UIViewRepresentable.swift b/Sources/UIKitBackend/UIViewRepresentable.swift index 5b20929cd9..75b5185466 100644 --- a/Sources/UIKitBackend/UIViewRepresentable.swift +++ b/Sources/UIKitBackend/UIViewRepresentable.swift @@ -1,5 +1,6 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit public struct UIViewRepresentableContext { public let coordinator: Coordinator diff --git a/Sources/UIKitBackend/Widget.swift b/Sources/UIKitBackend/Widget.swift index f315c80f73..3ebd6b40ed 100644 --- a/Sources/UIKitBackend/Widget.swift +++ b/Sources/UIKitBackend/Widget.swift @@ -1,4 +1,5 @@ import UIKit +import UIKitCompatKit public protocol WidgetProtocol: UIResponder { var x: Int { get set } From 3514861c42cfadbf3bca26149d857f8ad0ceed77 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 08:58:37 +1000 Subject: [PATCH 26/57] Make identifiable iOS 6+ rather than 8+ as it should be --- Sources/SwiftCrossUI/Views/List.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Views/List.swift b/Sources/SwiftCrossUI/Views/List.swift index cdfff42898..aeb4c711ab 100644 --- a/Sources/SwiftCrossUI/Views/List.swift +++ b/Sources/SwiftCrossUI/Views/List.swift @@ -1,5 +1,5 @@ //MARK: Will add to a shim library later, just here for testing now. -@available(iOS, introduced: 8.0, obsoleted: 13.0) +@available(iOS, introduced: 6.0, obsoleted: 13.0) public protocol Identifiable { associatedtype ID: Hashable var id: ID { get } From f8dac3d5461687864d624cb06d739091d2b00f5e Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:08:39 +1000 Subject: [PATCH 27/57] a --- Sources/UIKitBackend/UIKitBackend+Passive.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/UIKitBackend/UIKitBackend+Passive.swift b/Sources/UIKitBackend/UIKitBackend+Passive.swift index a753531a1a..80cf2c71e8 100644 --- a/Sources/UIKitBackend/UIKitBackend+Passive.swift +++ b/Sources/UIKitBackend/UIKitBackend+Passive.swift @@ -6,7 +6,7 @@ extension UIKitBackend { static func attributedString( text: String, environment: EnvironmentValues, - defaultForegroundColor: UIColor = .label + defaultForegroundColor: UIColor = .black //Changed for now ) -> NSAttributedString { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = From 808d145702aa1c84d2cb25071d8a8ba4d22cabce Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:10:02 +1000 Subject: [PATCH 28/57] a --- Sources/UIKitBackend/UIKitBackend+Passive.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/UIKitBackend/UIKitBackend+Passive.swift b/Sources/UIKitBackend/UIKitBackend+Passive.swift index 80cf2c71e8..35a9c10446 100644 --- a/Sources/UIKitBackend/UIKitBackend+Passive.swift +++ b/Sources/UIKitBackend/UIKitBackend+Passive.swift @@ -6,7 +6,7 @@ extension UIKitBackend { static func attributedString( text: String, environment: EnvironmentValues, - defaultForegroundColor: UIColor = .black //Changed for now + defaultForegroundColor: UIColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) //Changed for now ) -> NSAttributedString { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = From e03da4765479e0bfd631052b42c55b60ad34296d Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:17:31 +1000 Subject: [PATCH 29/57] testing --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f3be25be2b..29d8fc5772 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("7.0"), + .iOS("13.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 0618e6eff9ecd46572d6b4691d17f41a8344d57f Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:20:02 +1000 Subject: [PATCH 30/57] A --- Sources/SwiftCrossUI/Views/List.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/List.swift b/Sources/SwiftCrossUI/Views/List.swift index aeb4c711ab..8445b8a494 100644 --- a/Sources/SwiftCrossUI/Views/List.swift +++ b/Sources/SwiftCrossUI/Views/List.swift @@ -1,9 +1,9 @@ //MARK: Will add to a shim library later, just here for testing now. -@available(iOS, introduced: 6.0, obsoleted: 13.0) +/*@available(iOS, introduced: 6.0, obsoleted: 13.0) public protocol Identifiable { associatedtype ID: Hashable var id: ID { get } -} +}*/ public struct List: TypeSafeView, View { typealias Children = ListViewChildren> From 17048559c2964f85e3754cf7c6ac0d2825c68bb6 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:25:00 +1000 Subject: [PATCH 31/57] a --- Sources/UIKitBackend/UIColor+Color.swift | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/Sources/UIKitBackend/UIColor+Color.swift b/Sources/UIKitBackend/UIColor+Color.swift index 0c234fd383..ed2777514e 100644 --- a/Sources/UIKitBackend/UIColor+Color.swift +++ b/Sources/UIKitBackend/UIColor+Color.swift @@ -30,21 +30,11 @@ extension Color { } var cgColor: CGColor { - if #available(iOS 13.0, *) { - CGColor( - red: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) - } else { - //MARK: this won't work right now, get back to me on it. - CGColorShim( - red: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) as! CGColor - } + CGColor( + red: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) } } From 126be745efd2226429c4c55588470a1497e12c9b Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:37:09 +1000 Subject: [PATCH 32/57] changed some stuff in widget --- Package.resolved | 2 +- Package.swift | 2 +- Sources/SwiftCrossUI/Views/List.swift | 4 ++-- Sources/UIKitBackend/UIColor+Color.swift | 22 ++++++++++++++++------ Sources/UIKitBackend/Widget.swift | 13 +++++++++++-- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Package.resolved b/Package.resolved index 46defca676..05be13c430 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,7 +24,7 @@ "location" : "https://github.com/JWIMaster/UIKitCompatKit", "state" : { "branch" : "master", - "revision" : "0587a8892c66b3ac321f1847581a6b4752f5655a" + "revision" : "c93309c493b3943260a86f0a0533222540945e2b" } }, { diff --git a/Package.swift b/Package.swift index 29d8fc5772..f3be25be2b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("13.0"), + .iOS("7.0"), .tvOS(.v13), .macCatalyst(.v13) ], diff --git a/Sources/SwiftCrossUI/Views/List.swift b/Sources/SwiftCrossUI/Views/List.swift index 8445b8a494..aeb4c711ab 100644 --- a/Sources/SwiftCrossUI/Views/List.swift +++ b/Sources/SwiftCrossUI/Views/List.swift @@ -1,9 +1,9 @@ //MARK: Will add to a shim library later, just here for testing now. -/*@available(iOS, introduced: 6.0, obsoleted: 13.0) +@available(iOS, introduced: 6.0, obsoleted: 13.0) public protocol Identifiable { associatedtype ID: Hashable var id: ID { get } -}*/ +} public struct List: TypeSafeView, View { typealias Children = ListViewChildren> diff --git a/Sources/UIKitBackend/UIColor+Color.swift b/Sources/UIKitBackend/UIColor+Color.swift index ed2777514e..0c234fd383 100644 --- a/Sources/UIKitBackend/UIColor+Color.swift +++ b/Sources/UIKitBackend/UIColor+Color.swift @@ -30,11 +30,21 @@ extension Color { } var cgColor: CGColor { - CGColor( - red: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) + if #available(iOS 13.0, *) { + CGColor( + red: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) + } else { + //MARK: this won't work right now, get back to me on it. + CGColorShim( + red: CGFloat(red), + green: CGFloat(green), + blue: CGFloat(blue), + alpha: CGFloat(alpha) + ) as! CGColor + } } } diff --git a/Sources/UIKitBackend/Widget.swift b/Sources/UIKitBackend/Widget.swift index 3ebd6b40ed..3366688780 100644 --- a/Sources/UIKitBackend/Widget.swift +++ b/Sources/UIKitBackend/Widget.swift @@ -289,11 +289,20 @@ class WrapperWidget: BaseViewWidget { self.addSubview(child) child.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - child.topAnchor.constraint(equalTo: self.topAnchor), + { + let c = child.topAnchor.constraint(equalTo: self.topAnchor) + c.priority = .defaultHigh + return c + }(), child.leadingAnchor.constraint(equalTo: self.leadingAnchor), - child.bottomAnchor.constraint(equalTo: self.bottomAnchor), + { + let c = child.bottomAnchor.constraint(equalTo: self.bottomAnchor) + c.priority = .defaultHigh + return c + }(), child.trailingAnchor.constraint(equalTo: self.trailingAnchor), ]) + } override convenience init() { From b7483d86e5b0991292c6d20be944e220043c4586 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:39:12 +1000 Subject: [PATCH 33/57] test --- Sources/UIKitBackend/Widget.swift | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Sources/UIKitBackend/Widget.swift b/Sources/UIKitBackend/Widget.swift index 3366688780..7ca202ce08 100644 --- a/Sources/UIKitBackend/Widget.swift +++ b/Sources/UIKitBackend/Widget.swift @@ -166,7 +166,7 @@ class BaseViewWidget: UIView, WidgetProtocolHelpers { super.didMoveToSuperview() updateLeftConstraint() - updateTopConstraint() + //updateTopConstraint() } func add(childWidget: some WidgetProtocol) { @@ -289,20 +289,11 @@ class WrapperWidget: BaseViewWidget { self.addSubview(child) child.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - { - let c = child.topAnchor.constraint(equalTo: self.topAnchor) - c.priority = .defaultHigh - return c - }(), + child.topAnchor.constraint(equalTo: self.topAnchor), child.leadingAnchor.constraint(equalTo: self.leadingAnchor), - { - let c = child.bottomAnchor.constraint(equalTo: self.bottomAnchor) - c.priority = .defaultHigh - return c - }(), + child.bottomAnchor.constraint(equalTo: self.bottomAnchor), child.trailingAnchor.constraint(equalTo: self.trailingAnchor), ]) - } override convenience init() { From 20919006b6c9de7e3b9ee5228c6455a62573ceae Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:40:40 +1000 Subject: [PATCH 34/57] a --- Package.swift | 2 +- Sources/UIKitBackend/Widget.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index f3be25be2b..277080a789 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("7.0"), + .iOS("9.0"), .tvOS(.v13), .macCatalyst(.v13) ], diff --git a/Sources/UIKitBackend/Widget.swift b/Sources/UIKitBackend/Widget.swift index 7ca202ce08..3ebd6b40ed 100644 --- a/Sources/UIKitBackend/Widget.swift +++ b/Sources/UIKitBackend/Widget.swift @@ -166,7 +166,7 @@ class BaseViewWidget: UIView, WidgetProtocolHelpers { super.didMoveToSuperview() updateLeftConstraint() - //updateTopConstraint() + updateTopConstraint() } func add(childWidget: some WidgetProtocol) { From 5611c16239cc0363c289f08d3e13e1aec27ddfe0 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:42:33 +1000 Subject: [PATCH 35/57] a --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 277080a789..d5e9338890 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("9.0"), + .iOS("11.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 585f253e2c5e119c2e1a3b06f695e6edcc516536 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:50:38 +1000 Subject: [PATCH 36/57] test --- Package.swift | 2 +- Sources/UIKitBackend/UIKitBackend+Menu.swift | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index d5e9338890..7437eed2a4 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("11.0"), + .iOS("12.0"), .tvOS(.v13), .macCatalyst(.v13) ], diff --git a/Sources/UIKitBackend/UIKitBackend+Menu.swift b/Sources/UIKitBackend/UIKitBackend+Menu.swift index 93996bc8ae..6da88c03f3 100644 --- a/Sources/UIKitBackend/UIKitBackend+Menu.swift +++ b/Sources/UIKitBackend/UIKitBackend+Menu.swift @@ -1,16 +1,19 @@ import SwiftCrossUI import UIKit -@available(iOS 13, *) + extension UIKitBackend { + @available(iOS 13, *) public final class Menu { var uiMenu: UIMenu? } - + + @available(iOS 13, *) public func createPopoverMenu() -> Menu { return Menu() } - + + @available(iOS 13, *) @available(tvOS 14, *) static func buildMenu( content: ResolvedMenu, @@ -32,7 +35,8 @@ extension UIKitBackend { return UIMenu(title: label, identifier: identifier, children: children) } - + + @available(iOS 13, *) public func updatePopoverMenu( _ menu: Menu, content: ResolvedMenu, environment _: EnvironmentValues ) { @@ -42,7 +46,8 @@ extension UIKitBackend { preconditionFailure("Current OS is too old to support menu buttons.") } } - + + @available(iOS 13, *) public func updateButton( _ button: Widget, label: String, @@ -59,7 +64,8 @@ extension UIKitBackend { preconditionFailure("Current OS is too old to support menu buttons.") } } - + + @available(iOS 13, *) public func setApplicationMenu(_ submenus: [ResolvedMenu.Submenu]) { #if targetEnvironment(macCatalyst) let appDelegate = UIApplication.shared.delegate as! ApplicationDelegate From 43a283d28765630d2bd9e30744f53295b039b69c Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 09:56:32 +1000 Subject: [PATCH 37/57] maybe this is what it was --- Sources/UIKitBackend/UIColor+Color.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/UIKitBackend/UIColor+Color.swift b/Sources/UIKitBackend/UIColor+Color.swift index 0c234fd383..ae798eaa5d 100644 --- a/Sources/UIKitBackend/UIColor+Color.swift +++ b/Sources/UIKitBackend/UIColor+Color.swift @@ -30,7 +30,7 @@ extension Color { } var cgColor: CGColor { - if #available(iOS 13.0, *) { + /*if #available(iOS 13.0, *) { CGColor( red: CGFloat(red), green: CGFloat(green), @@ -39,12 +39,10 @@ extension Color { ) } else { //MARK: this won't work right now, get back to me on it. - CGColorShim( - red: CGFloat(red), - green: CGFloat(green), - blue: CGFloat(blue), - alpha: CGFloat(alpha) - ) as! CGColor - } + + }*/ + let colorSpace = CGColorSpaceCreateDeviceRGB() + let components: [CGFloat] = [CGFloat(red), CGFloat(green), CGFloat(blue), CGFloat(alpha)] + return CGColor(colorSpace: colorSpace, components: components)! } } From 86bbe3c4eea7be7c837e790d33e2c91160aca8aa Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:07:53 +1000 Subject: [PATCH 38/57] testing stuff --- Sources/UIKitBackend/UIKitBackend+Control.swift | 7 +++++-- Sources/UIKitBackend/UIKitBackend+Window.swift | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index fc1dd69925..ed6747823e 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -244,14 +244,17 @@ extension UIKitBackend { // ignore foreground color for buttons on tvOS until we have a better // solution. let foregroundColor: UIColor - if #available(iOS 13, *) { + foregroundColor = .blue + + //MARK: Test fix + /*if #available(iOS 13, *) { foregroundColor = .link } else if #available(iOS 7, *) { // fallback color for older iOS versions foregroundColor = UIColor.systemBlue } else { foregroundColor = UIColor.blue - } + }*/ #if os(tvOS) buttonWidget.child.setTitle(label, for: .normal) diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 2ba687268a..c79c4ee53a 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -37,7 +37,7 @@ final class RootViewController: UIViewController { override func loadView() { super.loadView() - if #available(iOS 12, *) { + if #available(iOS 13, *) { if traitCollection.userInterfaceStyle != .dark { view.backgroundColor = .white } @@ -158,6 +158,10 @@ extension UIKitBackend { if #available(iOS 13, *) { window.windowScene?.sizeRestrictions?.minimumSize = CGSize( width: CGFloat(minimumSize.x), height: CGFloat(minimumSize.y)) + } else { + // iOS 12: windowScene/sizeRestrictions not available + // Optional: enforce min size in your layout logic + print("UIKitBackend: setMinimumSize ignored on iOS < 13") } } } From e213f158ca9c353967c36671c5af73f49f64bdef Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:21:13 +1000 Subject: [PATCH 39/57] maybe its menu stuff? --- Sources/SwiftCrossUI/_App.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index 4d0a0a3624..cbadc2d790 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -87,7 +87,7 @@ class _App { environment: self.environment ) - self.backend.setApplicationMenu(body.commands.resolve()) + //self.backend.setApplicationMenu(body.commands.resolve()) } self.cancellables.append(cancellable) } @@ -107,7 +107,7 @@ class _App { } // Update application-wide menu - self.backend.setApplicationMenu(body.commands.resolve()) + //self.backend.setApplicationMenu(body.commands.resolve()) rootNode.update( nil, From bd0ba38c1516507facea37d3414548fb6ccbf8db Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:22:27 +1000 Subject: [PATCH 40/57] it's menu lol --- Sources/SwiftCrossUI/_App.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index cbadc2d790..c8f0915c38 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -87,7 +87,9 @@ class _App { environment: self.environment ) - //self.backend.setApplicationMenu(body.commands.resolve()) + if #available(iOS 13.0, *) { + self.backend.setApplicationMenu(body.commands.resolve()) + } } self.cancellables.append(cancellable) } @@ -107,7 +109,9 @@ class _App { } // Update application-wide menu - //self.backend.setApplicationMenu(body.commands.resolve()) + if #available(iOS 13.0, *) { + self.backend.setApplicationMenu(body.commands.resolve()) + } rootNode.update( nil, From 6ee8953332cd9b0d10913fa4117a97ebd03bd25f Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:23:01 +1000 Subject: [PATCH 41/57] take back to 7, hope it works --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7437eed2a4..f3be25be2b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("12.0"), + .iOS("7.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 0bc3fe42c2b53a27db38b4a3877265914d97c8fe Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:25:07 +1000 Subject: [PATCH 42/57] wtf --- Sources/UIKitBackend/UIKitBackend+WebView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/UIKitBackend/UIKitBackend+WebView.swift b/Sources/UIKitBackend/UIKitBackend+WebView.swift index 922484d2ea..0d562139a8 100644 --- a/Sources/UIKitBackend/UIKitBackend+WebView.swift +++ b/Sources/UIKitBackend/UIKitBackend+WebView.swift @@ -1,6 +1,7 @@ import SwiftCrossUI import WebKit +/* @available(iOS 8, *) extension UIKitBackend { public func createWebView() -> Widget { @@ -42,4 +43,4 @@ final class WebViewWidget: WrapperWidget, WKNavigationDelegate { onNavigate?(url) } -} +}*/ From 81306e3365eb2d9a5ce653b0c812daf6a1e6fa6a Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:26:27 +1000 Subject: [PATCH 43/57] still not ios 7,gonna try ios 9 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f3be25be2b..277080a789 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("7.0"), + .iOS("9.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 988240edfec472067485d203cac5d4397a795dff Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:27:33 +1000 Subject: [PATCH 44/57] forgot there's some broken stuff --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 277080a789..d5e9338890 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("9.0"), + .iOS("11.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 63efc213be22a3daf91a7560f934d83f1c999313 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:28:33 +1000 Subject: [PATCH 45/57] back to ios 12 to test and verify --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index d5e9338890..7437eed2a4 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("11.0"), + .iOS("12.0"), .tvOS(.v13), .macCatalyst(.v13) ], From 95b1bb72cc5ced91bfe163e1314a2e2f6da1b59e Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:30:46 +1000 Subject: [PATCH 46/57] ? --- Sources/SwiftCrossUI/_App.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index c8f0915c38..cbadc2d790 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -87,9 +87,7 @@ class _App { environment: self.environment ) - if #available(iOS 13.0, *) { - self.backend.setApplicationMenu(body.commands.resolve()) - } + //self.backend.setApplicationMenu(body.commands.resolve()) } self.cancellables.append(cancellable) } @@ -109,9 +107,7 @@ class _App { } // Update application-wide menu - if #available(iOS 13.0, *) { - self.backend.setApplicationMenu(body.commands.resolve()) - } + //self.backend.setApplicationMenu(body.commands.resolve()) rootNode.update( nil, From eb2e18e8bf8cf0aeb88470cdca0e3695eead2d73 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:31:53 +1000 Subject: [PATCH 47/57] okay so I can't get rid of that function in the usual way, will have to investigate --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7437eed2a4..f3be25be2b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( name: "swift-cross-ui", platforms: [ .macOS(.v10_15), - .iOS("12.0"), + .iOS("7.0"), .tvOS(.v13), .macCatalyst(.v13) ], From b738624f774539b8590cc5fa41a79defe8575aa5 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 10:46:20 +1000 Subject: [PATCH 48/57] Commented out all concurrency, for some reason the app has some kind of weak dependency on it --- .../Environment/Actions/PresentAlertAction.swift | 4 ++-- .../Environment/Actions/PresentFileSaveDialogAction.swift | 4 ++-- .../Actions/PresentSingleFileOpenDialogAction.swift | 4 ++-- .../Views/Modifiers/Lifecycle/OnDisappearModifier.swift | 4 ++-- .../Views/Modifiers/Lifecycle/TaskModifier.swift | 4 ++-- Tests/SwiftCrossUITests/PublisherTests.swift | 4 +++- Tests/SwiftCrossUITests/SwiftCrossUITests.swift | 6 ++++-- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift index cd7dab9d71..9b99920a99 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift @@ -8,7 +8,7 @@ public struct PresentAlertAction { let environment: EnvironmentValues // MARK: - iOS 13+ version with async/await - @available(iOS 13.0, *) + /*@available(iOS 13.0, *) @discardableResult public func callAsFunction( _ title: String, @@ -43,7 +43,7 @@ public struct PresentAlertAction { } return await presentAlert(backend: environment.backend) - } + }*/ // MARK: - iOS 12 and below version with completion handler @discardableResult diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift index bcc1fb428c..8891572b81 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift @@ -6,7 +6,7 @@ public struct PresentFileSaveDialogAction: Sendable { let backend: any AppBackend let window: MainActorBox - @available(iOS 13, *) + /*@available(iOS 13, *) public func callAsFunction( title: String = "Save", message: String = "", @@ -53,7 +53,7 @@ public struct PresentFileSaveDialogAction: Sendable { } return await chooseFile(backend: backend) - } + }*/ // MARK: - iOS 12 and below version with completion handler public func callAsFunction( diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift index fec874eaa3..315a57c957 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift @@ -8,7 +8,7 @@ public struct PresentSingleFileOpenDialogAction { let window: MainActorBox // MARK: - iOS 13+ async/await version - @available(iOS 13.0, *) + /*@available(iOS 13.0, *) public func callAsFunction( title: String = "Open", message: String = "", @@ -56,7 +56,7 @@ public struct PresentSingleFileOpenDialogAction { } return await chooseFile(backend: backend) - } + }*/ // MARK: - iOS 12 and below version with completion handler public func callAsFunction( diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift index 9af2cbce98..11c20d8804 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift @@ -89,9 +89,9 @@ class OnDisappearModifierChildren: ViewGraphNodeChildren { deinit { if #available(iOS 13, *) { - Task { @MainActor [action] in + /*Task { @MainActor [action] in action() - } + }*/ } else { DispatchQueue.main.async { self.action() diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift index f81934777a..db1ab72f75 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/TaskModifier.swift @@ -1,6 +1,6 @@ //MARK: Is this needed? This seems to be the worst offender in terms of compatibility. -@available(iOS 13, *) +/*@available(iOS 13, *) extension View { /// Starts a task before a view appears (but after ``View/body`` has been /// accessed), and cancels the task when the view disappears. Additionally, @@ -64,4 +64,4 @@ extension TaskModifier: View { task?.cancel() } } -} +}*/ diff --git a/Tests/SwiftCrossUITests/PublisherTests.swift b/Tests/SwiftCrossUITests/PublisherTests.swift index acff150562..6ba69b4ff5 100644 --- a/Tests/SwiftCrossUITests/PublisherTests.swift +++ b/Tests/SwiftCrossUITests/PublisherTests.swift @@ -62,6 +62,8 @@ struct PublisherTests { #expect(!observedChange, "Expected mutation not to trigger cancelled observation") } + + /* #if canImport(AppKitBackend) // TODO: Create mock backend so that this can be tested on all platforms. There's // nothing AppKit-specific about it. @@ -118,5 +120,5 @@ struct PublisherTests { """ ) } - #endif + #endif*/ } diff --git a/Tests/SwiftCrossUITests/SwiftCrossUITests.swift b/Tests/SwiftCrossUITests/SwiftCrossUITests.swift index e88a827aee..e8d32464fc 100644 --- a/Tests/SwiftCrossUITests/SwiftCrossUITests.swift +++ b/Tests/SwiftCrossUITests/SwiftCrossUITests.swift @@ -69,7 +69,9 @@ struct SwiftCrossUITests { return original == decoded } - + + + /* #if canImport(AppKitBackend) @Test("Ensure that a basic view has the expected dimensions under AppKitBackend") @MainActor @@ -129,5 +131,5 @@ struct SwiftCrossUITests { return data } - #endif + #endif*/ } From 649a9f45d2a7c6a39c370ba8747e01cbcaa205d0 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:03:56 +1000 Subject: [PATCH 49/57] idek anymore --- Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index b80b848699..3f73a9823e 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -168,12 +168,15 @@ public class ViewGraphNode: Sendable { } private func updateEnvironment(_ environment: EnvironmentValues) -> EnvironmentValues { - environment.with(\.onResize) { [weak self] _ in + var newEnvironment = environment + newEnvironment.with(\.onResize) { [weak self] _ in guard let self = self else { return } self.bottomUpUpdate() } + return newEnvironment } + /// Recomputes the view's body, and updates its widget accordingly. The view may or may not /// propagate the update to its children depending on the nature of the update. If `newView` /// is provided (in the case that the parent's body got updated) then it simply replaces the From c95faa338ef0af76f398f76b02a54e3a1366abfd Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:06:36 +1000 Subject: [PATCH 50/57] a --- Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index 3f73a9823e..7c8e1bee4f 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -169,7 +169,8 @@ public class ViewGraphNode: Sendable { private func updateEnvironment(_ environment: EnvironmentValues) -> EnvironmentValues { var newEnvironment = environment - newEnvironment.with(\.onResize) { [weak self] _ in + // Directly assign the closure instead of using keypath + newEnvironment.onResize = { [weak self] newSize in guard let self = self else { return } self.bottomUpUpdate() } From 1e4285303bd312febb8c660b3f57c41775a3fc49 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:08:23 +1000 Subject: [PATCH 51/57] idk why this breaks stuff but /shrug --- .../SwiftCrossUI/Scenes/WindowGroupNode.swift | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 717906b484..41a28103ac 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -120,21 +120,27 @@ public final class WindowGroupNode: SceneGraphNode { scene = newScene } - let environment = - backend.computeWindowEnvironment(window: window, rootEnvironment: environment) - .with(\.onResize) { [weak self] _ in - guard let self = self else { return } - // TODO: Figure out whether this would still work if we didn't recompute the - // scene's body. I have a vague feeling that it wouldn't work in all cases? - // But I don't have the time to come up with a counterexample right now. - _ = self.update( - self.scene, - proposedWindowSize: backend.size(ofWindow: window), - backend: backend, - environment: environment - ) - } - .with(\.window, window) + var newEnvironment = backend.computeWindowEnvironment( + window: window, + rootEnvironment: environment + ) + + // Assign onResize manually + newEnvironment.onResize = { [weak self] _ in + guard let self = self else { return } + _ = self.update( + self.scene, + proposedWindowSize: backend.size(ofWindow: window), + backend: backend, + environment: newEnvironment + ) + } + + // Assign window manually + newEnvironment.window = window + + let environment = newEnvironment + let dryRunResult: ViewUpdateResult? if !windowSizeIsFinal { From a68ced4a43c50db76b262f2911ebc16865ebc315 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:11:50 +1000 Subject: [PATCH 52/57] i sure hope there isnt a lot of these --- .../ViewGraph/PreferenceValues.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift index d03e497a39..5cb27dbf2a 100644 --- a/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift +++ b/Sources/SwiftCrossUI/ViewGraph/PreferenceValues.swift @@ -12,14 +12,22 @@ public struct PreferenceValues: Sendable { } public init(merging children: [PreferenceValues]) { - let handlers = children.compactMap(\.onOpenURL) + // Extract all non-nil handlers from children + let handlers: [(URL) -> Void] = children.compactMap { $0.onOpenURL } - if !handlers.isEmpty { - onOpenURL = { url in - for handler in handlers { + // Assign a closure that safely calls each handler + onOpenURL = { url in + for handler in handlers { + // Wrap each call in a do/catch to prevent crashes from unexpected errors + // or weak captures inside handlers + do { handler(url) + } catch { + // Optionally log the error, but prevent crash + print("Warning: onOpenURL handler threw an error: \(error)") } } } } + } From 0794d2cbcf003fa32bb4546bc35906a7a0d42106 Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:32:12 +1000 Subject: [PATCH 53/57] idk --- Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index 7c8e1bee4f..287ea50090 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -169,15 +169,17 @@ public class ViewGraphNode: Sendable { private func updateEnvironment(_ environment: EnvironmentValues) -> EnvironmentValues { var newEnvironment = environment - // Directly assign the closure instead of using keypath - newEnvironment.onResize = { [weak self] newSize in - guard let self = self else { return } + + // Strong capture of self (no weak) + newEnvironment.onResize = { newSize in self.bottomUpUpdate() } + return newEnvironment } + /// Recomputes the view's body, and updates its widget accordingly. The view may or may not /// propagate the update to its children depending on the nature of the update. If `newView` /// is provided (in the case that the parent's body got updated) then it simply replaces the From 55ea3eb6670b507d8cd750c11ec275efa8a12a0d Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:35:32 +1000 Subject: [PATCH 54/57] removing weak self captures for debugging --- .../SwiftCrossUI/Scenes/WindowGroupNode.swift | 52 ++----------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 41a28103ac..03fe8a3a09 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -44,10 +44,7 @@ public final class WindowGroupNode: SceneGraphNode { self.window = window parentEnvironment = environment - backend.setResizeHandler(ofWindow: window) { [weak self] newSize in - guard let self else { - return - } + backend.setResizeHandler(ofWindow: window) { newSize in _ = self.update( self.scene, proposedWindowSize: newSize, @@ -58,10 +55,7 @@ public final class WindowGroupNode: SceneGraphNode { ) } - backend.setWindowEnvironmentChangeHandler(of: window) { [weak self] in - guard let self else { - return - } + backend.setWindowEnvironmentChangeHandler(of: window) { _ = self.update( self.scene, proposedWindowSize: backend.size(ofWindow: window), @@ -110,11 +104,6 @@ public final class WindowGroupNode: SceneGraphNode { parentEnvironment = environment if let newScene = newScene { - // Don't set default size even if it has changed. We only set that once - // at window creation since some backends don't have a concept of - // 'default' size which would mean that setting the default size every time - // the default size changed would resize the window (which is incorrect - // behaviour). backend.setTitle(ofWindow: window, to: newScene.title) backend.setResizability(ofWindow: window, to: newScene.resizability.isResizable) scene = newScene @@ -125,9 +114,8 @@ public final class WindowGroupNode: SceneGraphNode { rootEnvironment: environment ) - // Assign onResize manually - newEnvironment.onResize = { [weak self] _ in - guard let self = self else { return } + // Assign onResize manually, strong capture + newEnvironment.onResize = { _ in _ = self.update( self.scene, proposedWindowSize: backend.size(ofWindow: window), @@ -144,8 +132,6 @@ public final class WindowGroupNode: SceneGraphNode { let dryRunResult: ViewUpdateResult? if !windowSizeIsFinal { - // Perform a dry-run update of the root view to check if the window - // needs to change size. let contentResult = viewGraph.update( with: newScene?.body, proposedSize: proposedWindowSize, @@ -161,9 +147,6 @@ public final class WindowGroupNode: SceneGraphNode { environment: environment ) - // Restart the window update if the content has caused the window to - // change size. To avoid infinite recursion, we take the view's word - // and assume that it will take on the minimum/maximum size it claimed. if let newWindowSize { return update( scene, @@ -184,40 +167,17 @@ public final class WindowGroupNode: SceneGraphNode { dryRun: false ) - // The Gtk 3 backend has some broken sizing code that can't really be - // fixed due to the design of Gtk 3. Our layout system underestimates - // the size of the new view due to the button not being in the Gtk 3 - // widget hierarchy yet (which prevents Gtk 3 from computing the - // natural sizes of the new buttons). One fix seems to be removing - // view size reuse (currently the second check in ViewGraphNode.update) - // and I'm not exactly sure why, but that makes things awfully slow. - // The other fix is to add an alternative path to - // Gtk3Backend.naturalSize(of:) for buttons that moves non-realized - // buttons to a secondary window before measuring their natural size, - // but that's super janky, easy to break if the button in the real - // window is inheriting styles from its ancestors, and I'm not sure - // how to hide the window (it's probably terrible for performance too). - // - // I still have no clue why this size underestimation (and subsequent - // mis-sizing of the window) had the symptom of all buttons losing - // their labels temporarily; Gtk 3 is a temperamental beast. - // - // Anyway, Gtk3Backend isn't really intended to be a recommended - // backend so I think this is a fine solution for now (people should - // only use Gtk3Backend if they can't use GtkBackend). if let dryRunResult, finalContentResult.size != dryRunResult.size { print( """ warning: Final window content size didn't match dry-run size. This is a sign that - either view size caching is broken or that backend.naturalSize(of:) is + either view size caching is broken or that backend.naturalSize(of:) is broken (or both). -> dryRunResult.size: \(dryRunResult.size) -> finalContentResult.size: \(finalContentResult.size) """ ) - // Give the view graph one more chance to sort itself out to fail - // as gracefully as possible. let newWindowSize = computeNewWindowSize( currentProposedSize: proposedWindowSize, backend: backend, @@ -236,8 +196,6 @@ public final class WindowGroupNode: SceneGraphNode { } } - // Set this even if the window isn't programmatically resizable - // because the window may still be user resizable. if scene.resizability.isResizable { backend.setMinimumSize( ofWindow: window, From b23b7e4ef008f7aac346f90589a1c1efca8362bf Mon Sep 17 00:00:00 2001 From: JWI Date: Fri, 3 Oct 2025 14:40:58 +1000 Subject: [PATCH 55/57] Please note, the weak self captures previously in here 1, don't seem to serve a purpose and 2, break compatibility with iOS 6, as they implicitly call atomic_store. This is now patched by removing them --- Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 03fe8a3a09..259ac2bee3 100644 --- a/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift +++ b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift @@ -167,6 +167,7 @@ public final class WindowGroupNode: SceneGraphNode { dryRun: false ) + if let dryRunResult, finalContentResult.size != dryRunResult.size { print( """ From 8077a7ccadfac8063b8e089c068ad2c1ec98b3aa Mon Sep 17 00:00:00 2001 From: JWI Date: Sat, 4 Oct 2025 16:27:10 +1000 Subject: [PATCH 56/57] test to see if mainactor needs to be removed --- .../Views/Modifiers/Lifecycle/OnAppearModifier.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnAppearModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnAppearModifier.swift index 79c6479aca..cf673c84fa 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnAppearModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnAppearModifier.swift @@ -6,14 +6,14 @@ extension View { /// view's ``View/body`` and before the view appears on screen. Currently, /// if these docs have been kept up to date, the action gets called just /// before creating the view's widget. - public func onAppear(perform action: @escaping @MainActor () -> Void) -> some View { + public func onAppear(perform action: @escaping () -> Void) -> some View { OnAppearModifier(body: TupleView1(self), action: action) } } struct OnAppearModifier: View { var body: TupleView1 - var action: @MainActor () -> Void + var action: () -> Void func asWidget( _ children: any ViewGraphNodeChildren, From f0801e6878c79140a71baa7dad37dbb9676bc8c7 Mon Sep 17 00:00:00 2001 From: JWI Date: Sat, 4 Oct 2025 16:33:06 +1000 Subject: [PATCH 57/57] remove --- Sources/SwiftCrossUI/Views/Button.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index e3dec57c22..88d9691ab8 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -3,12 +3,12 @@ public struct Button: Sendable { /// The label to show on the button. public var label: String /// The action to be performed when the button is clicked. - public var action: @MainActor @Sendable () -> Void + public var action: @Sendable () -> Void /// The button's forced width if provided. var width: Int? /// Creates a button that displays a custom label. - public init(_ label: String, action: @escaping @MainActor @Sendable () -> Void = {}) { + public init(_ label: String, action: @escaping @Sendable () -> Void = {}) { self.label = label self.action = action }