diff --git a/Package.resolved b/Package.resolved index 18390df54a..05be13c430 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,126 +1,30 @@ { - "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" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", "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" + "revision" : "72d3da66b085c2299dd287c2be3b92b5ebd226de", + "version" : "0.50700.1" } }, { - "identity" : "swift-windowsappsdk", + "identity" : "uikitcompatkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-windowsappsdk", + "location" : "https://github.com/JWIMaster/UIKitCompatKit", "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" + "branch" : "master", + "revision" : "c93309c493b3943260a86f0a0533222540945e2b" } }, { @@ -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 f24ca7956c..f3be25be2b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,45 +1,33 @@ -// 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 } else { #if os(macOS) defaultBackend = "AppKitBackend" - #elseif os(Windows) - defaultBackend = "WinUIBackend" #else defaultBackend = "GtkBackend" #endif } +// 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 - -var swiftSettings: [SwiftSetting] = [] -if hotReloadingEnabled { - swiftSettings += [ - .define("HOT_RELOADING_ENABLED") - ] -} +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": @@ -52,84 +40,37 @@ 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( name: "swift-cross-ui", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .macCatalyst(.v13), .visionOS(.v1)], + platforms: [ + .macOS(.v10_15), + .iOS("7.0"), + .tvOS(.v13), + .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: "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"]), .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.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( name: "SwiftCrossUI", - dependencies: [ - "HotReloadingMacrosPlugin", - .product(name: "ImageFormats", package: "swift-image-formats"), - ], exclude: [ "Builders/ViewBuilder.swift.gyb", "Builders/SceneBuilder.swift.gyb", @@ -138,9 +79,6 @@ let package = Package( "Views/TupleViewChildren.swift.gyb", "Views/TableRowContent.swift.gyb", "Scenes/TupleScene.swift.gyb", - ], - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency") ] ), .testTarget( @@ -153,28 +91,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])), + .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 +106,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,85 +118,19 @@ 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: "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: [] - ), - // .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: "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", + .product(name: "UIKitCompatKit", package: "UIKitCompatKit"), + ]), ] ) 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") @@ -299,9 +139,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 diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift index a045493766..9b99920a99 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) } @@ -42,5 +43,41 @@ 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/Environment/Actions/PresentFileSaveDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift index 5905650011..8891572b81 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 = "", @@ -52,5 +53,53 @@ 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) } } diff --git a/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift b/Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift index 330bba3a47..315a57c957 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) } @@ -54,5 +56,54 @@ 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) } } 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/Scenes/WindowGroupNode.swift b/Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift index 717906b484..259ac2bee3 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,36 +104,34 @@ 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 } - 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, strong capture + newEnvironment.onResize = { _ in + _ = 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 { - // 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, @@ -155,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, @@ -178,40 +167,18 @@ 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, @@ -230,8 +197,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, 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/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)") } } } } + } diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index b80b848699..287ea50090 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -168,12 +168,18 @@ public class ViewGraphNode: Sendable { } private func updateEnvironment(_ environment: EnvironmentValues) -> EnvironmentValues { - environment.with(\.onResize) { [weak self] _ in - guard let self = self else { return } + var newEnvironment = environment + + // 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 diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index 739d1ba407..88d9691ab8 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -1,14 +1,14 @@ /// 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: @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 } diff --git a/Sources/SwiftCrossUI/Views/Image.swift b/Sources/SwiftCrossUI/Views/Image.swift index 2f3be3e6f8..a0fcc6762d 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: [UInt8](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 diff --git a/Sources/SwiftCrossUI/Views/List.swift b/Sources/SwiftCrossUI/Views/List.swift index 4283e5249e..aeb4c711ab 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: 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/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/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, diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Lifecycle/OnDisappearModifier.swift index 4e0a5f413c..11c20d8804 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 62726cf32a..db1ab72f75 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. + +/*@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, @@ -34,6 +37,7 @@ extension View { } } +@available(iOS 13, *) struct TaskModifier { @State var task: Task<(), any Error>? = nil @@ -43,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 @@ -59,4 +64,4 @@ extension TaskModifier: View { task?.cancel() } } -} +}*/ 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 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, 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..0d88b1a844 100644 --- a/Sources/UIKitBackend/Font+UIFont.swift +++ b/Sources/UIKitBackend/Font+UIFont.swift @@ -1,49 +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 { - case .monospaced: - uiFont = .monospacedSystemFont(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..ae798eaa5d 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,19 @@ 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. + + }*/ + let colorSpace = CGColorSpaceCreateDeviceRGB() + let components: [CGFloat] = [CGFloat(red), CGFloat(green), CGFloat(blue), CGFloat(alpha)] + return CGColor(colorSpace: colorSpace, components: components)! } } 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 9179075d20..ed6747823e 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,19 @@ 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 + 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) #else @@ -249,7 +263,7 @@ extension UIKitBackend { UIKitBackend.attributedString( text: label, environment: environment, - defaultForegroundColor: .link + defaultForegroundColor: foregroundColor ), for: .normal ) @@ -290,9 +304,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 +361,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 { @@ -354,20 +392,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 } @@ -383,6 +407,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?) { @@ -447,11 +472,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+Menu.swift b/Sources/UIKitBackend/UIKitBackend+Menu.swift index efe33affa9..6da88c03f3 100644 --- a/Sources/UIKitBackend/UIKitBackend+Menu.swift +++ b/Sources/UIKitBackend/UIKitBackend+Menu.swift @@ -1,15 +1,19 @@ import SwiftCrossUI import UIKit + 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, @@ -31,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 ) { @@ -41,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, @@ -58,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 diff --git a/Sources/UIKitBackend/UIKitBackend+Passive.swift b/Sources/UIKitBackend/UIKitBackend+Passive.swift index 235e7b893c..35a9c10446 100644 --- a/Sources/UIKitBackend/UIKitBackend+Passive.swift +++ b/Sources/UIKitBackend/UIKitBackend+Passive.swift @@ -1,11 +1,12 @@ import SwiftCrossUI import UIKit +import UIKitCompatKit extension UIKitBackend { static func attributedString( text: String, environment: EnvironmentValues, - defaultForegroundColor: UIColor = .label + defaultForegroundColor: UIColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) //Changed for now ) -> NSAttributedString { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = @@ -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+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+WebView.swift b/Sources/UIKitBackend/UIKitBackend+WebView.swift index b4b74c062f..0d562139a8 100644 --- a/Sources/UIKitBackend/UIKitBackend+WebView.swift +++ b/Sources/UIKitBackend/UIKitBackend+WebView.swift @@ -1,6 +1,8 @@ import SwiftCrossUI import WebKit +/* +@available(iOS 8, *) extension UIKitBackend { public func createWebView() -> Widget { WebViewWidget() @@ -23,6 +25,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)? @@ -40,4 +43,4 @@ final class WebViewWidget: WrapperWidget, WKNavigationDelegate { onNavigate?(url) } -} +}*/ diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 46aa666ee7..c79c4ee53a 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 @@ -20,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?() @@ -34,11 +37,17 @@ final class RootViewController: UIViewController { override func loadView() { super.loadView() - if traitCollection.userInterfaceStyle != .dark { + if #available(iOS 13, *) { + 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 ) { @@ -146,7 +155,13 @@ 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)) + } else { + // iOS 12: windowScene/sizeRestrictions not available + // Optional: enforce min size in your layout logic + print("UIKitBackend: setMinimumSize ignored on iOS < 13") + } } } diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index f7580a6a06..823339665a 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 @@ -82,19 +82,27 @@ 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 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 @@ -125,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) } @@ -238,6 +247,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 +269,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 +293,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 { 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 } 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*/ }