diff --git a/Examples/Package.resolved b/Examples/Package.resolved index 3842945e2aa..cb59bcc5d55 100644 --- a/Examples/Package.resolved +++ b/Examples/Package.resolved @@ -220,8 +220,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log", "state" : { - "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", - "version" : "1.6.1" + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" } }, { diff --git a/Package.resolved b/Package.resolved index b260ee897fe..b91c2091dba 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "77caf3e84e88f2ff183c89c647d8007a7ba7b50bfb1fb7216b22f7dfda4a2dc0", + "originHash" : "9735f7da95f3aac9ace06a19f5c4e1a822b5a72397c47e857a072c23df62791b", "pins" : [ { "identity" : "jpeg", @@ -81,6 +81,15 @@ "version" : "0.3.3" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + }, { "identity" : "swift-macro-toolkit", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 2bc9c38117b..61564c6d949 100644 --- a/Package.swift +++ b/Package.swift @@ -126,6 +126,10 @@ let package = Package( url: "https://github.com/stackotter/swift-benchmark", .upToNextMinor(from: "0.2.0") ), + .package( + url: "https://github.com/apple/swift-log.git", + exact: "1.6.4" + ), // .package( // url: "https://github.com/stackotter/TermKit", // revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704" @@ -145,6 +149,7 @@ let package = Package( dependencies: [ "HotReloadingMacrosPlugin", .product(name: "ImageFormats", package: "swift-image-formats"), + .product(name: "Logging", package: "swift-log"), ], exclude: [ "Builders/ViewBuilder.swift.gyb", diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 1d2d2e7487e..346490289ca 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1,12 +1,30 @@ import AppKit +import Logging import SwiftCrossUI import WebKit +/// The storage behind `logger`. +/// +/// `nil` if the logger hasn't been set yet. +/// +/// > Safety: This is only set once, before it is ever read. +nonisolated(unsafe) private var _logger: Logger? + +/// The global logger for this backend. +var logger: Logger { + guard let _logger else { fatalError("logger not yet initialized") } + return _logger +} + extension App { public typealias Backend = AppKitBackend public var backend: AppKitBackend { - AppKitBackend() + _logger = Logger( + label: "AppKitBackend", + factory: Self.logHandler(label:metadataProvider:) + ) + return AppKitBackend() } } @@ -419,8 +437,7 @@ public final class AppKitBackend: AppBackend { public func setPosition(ofChildAt index: Int, in container: Widget, to position: SIMD2) { let container = container as! NSContainerView guard container.children.indices.contains(index) else { - // TODO: Create proper logging system. - print("warning: Attempted to set position of non-existent container child") + logger.warning("attempted to set position of non-existent container child") return } @@ -1259,7 +1276,7 @@ public final class AppKitBackend: AppBackend { } guard response != .abort, response != .cancel else { - print("warning: Got abort or cancel modal response, unexpected and unhandled") + logger.warning("got abort or cancel modal response, unexpected and unhandled") return } @@ -1956,11 +1973,11 @@ class NSCustomTableViewDelegate: NSObject, NSTableViewDelegate, NSTableViewDataS row: Int ) -> NSView? { guard let tableColumn else { - print("warning: No column provided") + logger.warning("no column provided") return nil } guard let columnIndex = columnIndices[ObjectIdentifier(tableColumn)] else { - print("warning: NSTableView asked for value of non-existent column") + logger.warning("NSTableView asked for value of non-existent column") return nil } return widgets[row * columnCount + columnIndex] @@ -2000,7 +2017,10 @@ extension ColorScheme { extension Color { init(_ nsColor: NSColor) { guard let resolvedNSColor = nsColor.usingColorSpace(.deviceRGB) else { - print("error: Failed to convert NSColor to RGB") + logger.error( + "failed to convert NSColor to RGB", + metadata: ["NSColor": "\(nsColor)"] + ) self = .black return } @@ -2291,7 +2311,7 @@ final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate { func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { guard let url = webView.url else { - print("warning: Web view has no URL") + logger.warning("web view has no URL") return } diff --git a/Sources/SwiftCrossUI/App.swift b/Sources/SwiftCrossUI/App.swift index 3da222101ce..62a43784bf5 100644 --- a/Sources/SwiftCrossUI/App.swift +++ b/Sources/SwiftCrossUI/App.swift @@ -1,4 +1,14 @@ import Foundation +import Logging + +/// Backing storage for `logger`. +nonisolated(unsafe) private var _logger: Logger? + +/// The global logger. +var logger: Logger { + guard let _logger else { fatalError("logger not yet initialized") } + return _logger +} /// An application. @MainActor @@ -8,9 +18,11 @@ public protocol App { /// The type of scene representing the content of the app. associatedtype Body: Scene - /// Metadata loaded at app start up. By default SwiftCrossUI attempts - /// to load metadata inserted by Swift Bundler if present. Used by backends' - /// default ``App/backend`` implementations if not `nil`. + /// Metadata loaded at app start up. + /// + /// By default SwiftCrossUI attempts to load metadata inserted by Swift + /// Bundler if present. Used by backends' default ``App/backend`` + /// implementations if not `nil`. static var metadata: AppMetadata? { get } /// The application's backend. @@ -20,7 +32,24 @@ public protocol App { @SceneBuilder var body: Body { get } /// Creates an instance of the app. + /// + /// This initializer is run before anything else, so you can perform early + /// setup tasks in here, such as opening a database or preparing a + /// dependency injection library. init() + + /// Returns the log handler to use for log messages emitted by SwiftCrossUI + /// and its backends. + /// + /// By default, SwiftCrossUI outputs log messages to standard error, but you + /// can use any log handler you want by implementing this requirement. + /// + /// # See Also + /// - + static func logHandler( + label: String, + metadataProvider: Logger.MetadataProvider? + ) -> any LogHandler } /// Force refresh the entire scene graph. Used by hot reloading. If you need to do @@ -28,38 +57,62 @@ public protocol App { @MainActor public var _forceRefresh: () -> Void = {} -/// Metadata embedded by Swift Bundler if present. Loaded at app start up. +/// Metadata embedded by Swift Bundler, if present. Loaded at app start up. +/// +/// This will contain the app's metadata, if present, by the time ``App/init()`` +/// gets called. @MainActor private var swiftBundlerAppMetadata: AppMetadata? /// An error encountered when parsing Swift Bundler metadata. private enum SwiftBundlerMetadataError: LocalizedError { + case noExecutableURL + case failedToReadExecutable case jsonNotDictionary(String) case missingAppIdentifier case missingAppVersion var errorDescription: String? { switch self { + case .noExecutableURL: + "no executable URL" + case .failedToReadExecutable: + "executable failed to read itself (to extract metadata)" case .jsonNotDictionary: - "Root metadata JSON value wasn't an object" + "root metadata JSON value wasn't an object" case .missingAppIdentifier: - "Missing 'appIdentifier' (of type String)" + "missing 'appIdentifier' (of type String)" case .missingAppVersion: - "Missing 'appVersion' (of type String)" + "missing 'appVersion' (of type String)" } } } extension App { /// Metadata loaded at app start up. + /// + /// This will contain the app's metadata, if present, by the time + /// ``App/init()`` gets called. public static var metadata: AppMetadata? { swiftBundlerAppMetadata } + /// The default log handler for apps which don't specify a custom one. + /// + /// This simply outputs logs to standard error. + /// + /// # See Also + /// - + public static func logHandler( + label: String, + metadataProvider: Logger.MetadataProvider? + ) -> any LogHandler { + StreamLogHandler.standardError(label: label) + } + /// Runs the application. public static func main() { - swiftBundlerAppMetadata = extractSwiftBundlerMetadata() - + extractMetadataAndInitializeLogging() let app = Self() let _app = _App(app) _forceRefresh = { @@ -70,15 +123,34 @@ extension App { _app.run() } - private static func extractSwiftBundlerMetadata() -> AppMetadata? { + private static func extractMetadataAndInitializeLogging() { + // Extract metadata _before_ initializing the logger, so users can use + // said metadata when declaring a custom logger. + let result = Result { + swiftBundlerAppMetadata = try extractSwiftBundlerMetadata() + } + + _logger = Logger( + label: "SwiftCrossUI", + factory: logHandler(label:metadataProvider:) + ) + + // Check for an error once the logger is ready. + if case .failure(let error) = result { + logger.error( + "failed to extract swift-bundler metadata", + metadata: ["error": "\(error.localizedDescription)"] + ) + } + } + + private static func extractSwiftBundlerMetadata() throws -> AppMetadata? { guard let executable = Bundle.main.executableURL else { - print("warning: No executable url") - return nil + throw SwiftBundlerMetadataError.noExecutableURL } guard let data = try? Data(contentsOf: executable) else { - print("warning: Executable failed to read self (to extract metadata)") - return nil + throw SwiftBundlerMetadataError.failedToReadExecutable } // Check if executable has Swift Bundler metadata magic bytes. @@ -92,33 +164,28 @@ extension App { let jsonStart = lengthStart - Int(jsonLength) let jsonData = Data(bytes[jsonStart.. \(error)") - return nil + // Manually parsed due to the `additionalMetadata` field (which would + // require a lot of boilerplate code to parse with Codable). + let jsonValue = try JSONSerialization.jsonObject(with: jsonData) + guard let json = jsonValue as? [String: Any] else { + throw SwiftBundlerMetadataError.jsonNotDictionary(String(describing: jsonValue)) + } + guard let identifier = json["appIdentifier"] as? String else { + throw SwiftBundlerMetadataError.missingAppIdentifier + } + guard let version = json["appVersion"] as? String else { + throw SwiftBundlerMetadataError.missingAppVersion } + let additionalMetadata = + json["additionalMetadata"].map { value in + value as? [String: Any] ?? [:] + } ?? [:] + + return AppMetadata( + identifier: identifier, + version: version, + additionalMetadata: additionalMetadata + ) } private static func parseBigEndianUInt64( diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index c0209a76e7e..6641ff9d71f 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -809,14 +809,14 @@ extension AppBackend { extension AppBackend { /// Used by placeholder implementations of backend methods. private func todo(_ function: String = #function) -> Never { - print("\(type(of: self)): \(function) not implemented") + logger.critical("\(type(of: self)): \(function) not implemented") Foundation.exit(1) } private func ignored(_ function: String = #function) { #if DEBUG - print( - "\(type(of: self)): \(function) is being ignored\nConsult at the documentation for further information." + logger.warning( + "\(type(of: self)): \(function) is being ignored; consult the documentation for further information" ) #endif } diff --git a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift index e3c3c8529d2..d6e7bbc3e84 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/DismissAction.swift @@ -37,7 +37,7 @@ private struct DismissActionKey: EnvironmentKey { static var defaultValue: DismissAction { DismissAction(action: { #if DEBUG - print("warning: dismiss() called but no presentation context is available") + logger.warning("dismiss() called but no presentation context is available") #endif }) } diff --git a/Sources/SwiftCrossUI/Environment/Actions/OpenURLAction.swift b/Sources/SwiftCrossUI/Environment/Actions/OpenURLAction.swift index 111058e72eb..3efeab506b1 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/OpenURLAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/OpenURLAction.swift @@ -11,7 +11,10 @@ public struct OpenURLAction { do { try backend.openExternalURL(url) } catch { - print("warning: Failed to open external url: \(error)") + logger.warning("failed to open external url", metadata: [ + "url": "\(url)", + "error": "\(error)", + ]) } } } diff --git a/Sources/SwiftCrossUI/Environment/Actions/RevealFileAction.swift b/Sources/SwiftCrossUI/Environment/Actions/RevealFileAction.swift index 30c7787af38..5f0efbbf8eb 100644 --- a/Sources/SwiftCrossUI/Environment/Actions/RevealFileAction.swift +++ b/Sources/SwiftCrossUI/Environment/Actions/RevealFileAction.swift @@ -15,7 +15,10 @@ public struct RevealFileAction { do { try backend.revealFile(file) } catch { - print("warning: Failed to reveal file: \(error)") + logger.warning("failed to reveal file", metadata: [ + "url": "\(file)", + "error": "\(error)", + ]) } } } diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index f9f5f1d2bec..c9a3183959a 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -127,7 +127,7 @@ public struct EnvironmentValues { backend.activate(window: window as! Backend.Window) } activate(with: backend) - print("Activated") + logger.info("window activated") } /// The backend's representation of the window that the current view is diff --git a/Sources/SwiftCrossUI/Layout/LayoutSystem.swift b/Sources/SwiftCrossUI/Layout/LayoutSystem.swift index e2473821bf7..3e9a8a61ad7 100644 --- a/Sources/SwiftCrossUI/Layout/LayoutSystem.swift +++ b/Sources/SwiftCrossUI/Layout/LayoutSystem.swift @@ -9,7 +9,7 @@ public enum LayoutSystem { package static func roundSize(_ size: Double) -> Int { if size.isInfinite { - print("warning: LayoutSystem.roundSize called with infinite size") + logger.warning("LayoutSystem.roundSize(_:) called with infinite size") } let size = size.rounded(.towardZero) @@ -197,11 +197,11 @@ public enum LayoutSystem { environment: environment ) if result.participatesInStackLayouts { - print( - """ - warning: Hidden view became visible on second update. \ - Layout may break. View: \(child.tag ?? "") - """ + logger.warning( + "hidden view became visible on second update; layout may break", + metadata: [ + "view": "\(child.tag ?? "")" + ] ) } renderedChildren[index] = result diff --git a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift b/Sources/SwiftCrossUI/State/DynamicKeyPath.swift index 9671ae4ebbd..ea3a247c526 100644 --- a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift +++ b/Sources/SwiftCrossUI/State/DynamicKeyPath.swift @@ -52,12 +52,18 @@ struct DynamicKeyPath { } guard let offset = matches.first else { - print("Warning: No offset found for dynamic property '\(label ?? "")'") + logger.warning( + "no offset found for dynamic property", + metadata: ["property": "\(label ?? "")"] + ) return nil } guard matches.count == 1 else { - print("Warning: Multiple offsets found for dynamic property '\(label ?? "")'") + logger.warning( + "multiple offsets found for dynamic property", + metadata: ["property": "\(label ?? "")"] + ) return nil } diff --git a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift index d1af87fe389..bab949376d4 100644 --- a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift +++ b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift @@ -66,11 +66,12 @@ struct DynamicPropertyUpdater { guard let updater = Self.getUpdater(for: value, base: base, label: label) else { // We have failed to create the required property updaters. Fallback // to using Mirrors to update all properties. - print( - """ - warning: Failed to produce DynamicPropertyUpdater for \(Base.self), \ - falling back to slower Mirror-based property updating approach. + logger.warning( """ + failed to produce DynamicPropertyUpdater; falling back to \ + slower Mirror-based property updating approach + """, + metadata: ["type": "\(Base.self)"] ) self.propertyUpdaters = nil diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/Logging.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Logging.md new file mode 100644 index 00000000000..8f804bd6ead --- /dev/null +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/Logging.md @@ -0,0 +1,15 @@ +# Logging + +SwiftCrossUI's logging mechanism + +SwiftCrossUI uses [swift-log](https://github.com/apple/swift-log) for logging +warnings. By default, it will log messages to the console; however, you can +use any log handler you want. To do so, add your handler of choice to your +package dependencies, then implement ``App/logHandler(label:metadataProvider:)-9yiqb`` +in your ``App`` conformance. + +- Tip: If you wish to use a separate log handler for any other libraries you may + use in your application, you can simply call `LoggingSystem.bootstrap(_:)` in + ``App/init()`` and pass it that handler. SwiftCrossUI uses the handler + returned by ``App/logHandler(label:metadataProvider:)-9yiqb`` directly, and + does not call `bootstrap(_:)` at any point. diff --git a/Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md b/Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md index 335e03d8dae..8a0610fb116 100644 --- a/Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md +++ b/Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md @@ -51,5 +51,6 @@ SwiftCrossUI takes inspiration from SwiftUI, allowing you to use the basic conce ### Other +- - - diff --git a/Sources/SwiftCrossUI/Values/TextStyle.swift b/Sources/SwiftCrossUI/Values/TextStyle.swift index 378a596e415..a129d5be866 100644 --- a/Sources/SwiftCrossUI/Values/TextStyle.swift +++ b/Sources/SwiftCrossUI/Values/TextStyle.swift @@ -52,17 +52,12 @@ extension Font.TextStyle { /// specify a text style for a specific platform. public func resolve(for deviceClass: DeviceClass) -> Resolved { guard let textStyles = Self.resolvedTextStyles[deviceClass] else { - print("warning: Missing text styles for device class \(deviceClass)") + logger.warning("missing text styles for device class \(deviceClass)") return .fallback } guard let textStyle = textStyles[self] else { - print( - """ - warning: Missing \(self) text style for device class \ - \(deviceClass) - """ - ) + logger.warning("missing \(self) text style for device class \(deviceClass)") return .fallback } diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift index 2d153c94595..dcadae00366 100644 --- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift +++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift @@ -110,13 +110,11 @@ public class ViewGraphNode: Sendable { // Update the view and its children when state changes (children are always updated first). for property in mirror.children { if property.label == "state" && property.value is ObservableObject { - print( + logger.warning( """ - - warning: The View.state protocol requirement has been removed in favour of - SwiftUI-style @State annotations. Decorate \(NodeView.self).state - with the @State property wrapper to restore previous behaviour. - + the View.state protocol requirement has been removed in favour of \ + SwiftUI-style @State annotations; decorate \(NodeView.self).state \ + with the @State property wrapper to restore previous behaviour """ ) } @@ -240,18 +238,24 @@ public class ViewGraphNode: Sendable { backend.show(widget: widget) guard let currentLayout else { - print("warning: layout committed before being computed, ignoring") + logger.warning("layout committed before being computed, ignoring") return .leafView(size: .zero) } if parentEnvironment.allowLayoutCaching { - print( - "warning: Committing layout computed with caching enabled. Results may be invalid. NodeView = \(NodeView.self)" + logger.warning( + "committing layout computed with caching enabled; results may be invalid", + metadata: ["NodeView": "\(NodeView.self)"] ) } if currentLayout.size.height == .infinity || currentLayout.size.width == .infinity { - print( - "warning: \(NodeView.self) has infinite height or width on commit, currentLayout.size: \(currentLayout.size), lastProposedSize: \(lastProposedSize)" + logger.warning( + "infinite height or width on commit", + metadata: [ + "NodeView": "\(NodeView.self)", + "currentLayout.size": "\(currentLayout.size)", + "lastProposedSize": "\(lastProposedSize)", + ] ) } diff --git a/Sources/SwiftCrossUI/Views/Group.swift b/Sources/SwiftCrossUI/Views/Group.swift index 1e92224e476..c2143070be3 100644 --- a/Sources/SwiftCrossUI/Views/Group.swift +++ b/Sources/SwiftCrossUI/Views/Group.swift @@ -31,7 +31,10 @@ public struct Group: View { backend: Backend ) -> ViewLayoutResult { if !(children is TupleViewChildren) { - print("warning: VStack will not function correctly non-TupleView Content") + logger.warning( + "Group will not function correctly with non-TupleView content", + metadata: ["childrenType": "\(type(of: children))"] + ) } var cache = (children as? TupleViewChildren)?.stackLayoutCache ?? StackLayoutCache() let result = LayoutSystem.computeStackLayout( diff --git a/Sources/SwiftCrossUI/Views/HStack.swift b/Sources/SwiftCrossUI/Views/HStack.swift index 36fe1f788d4..58784ba4701 100644 --- a/Sources/SwiftCrossUI/Views/HStack.swift +++ b/Sources/SwiftCrossUI/Views/HStack.swift @@ -37,7 +37,10 @@ public struct HStack: View { backend: Backend ) -> ViewLayoutResult { if !(children is TupleViewChildren) { - print("warning: VStack will not function correctly non-TupleView Content") + logger.warning( + "HStack will not function correctly with non-TupleView content", + metadata: ["childrenType": "\(type(of: children))"] + ) } var cache = (children as? TupleViewChildren)?.stackLayoutCache ?? StackLayoutCache() let result = LayoutSystem.computeStackLayout( diff --git a/Sources/SwiftCrossUI/Views/ScrollView.swift b/Sources/SwiftCrossUI/Views/ScrollView.swift index caa14b14e91..828f12a2b4b 100644 --- a/Sources/SwiftCrossUI/Views/ScrollView.swift +++ b/Sources/SwiftCrossUI/Views/ScrollView.swift @@ -72,7 +72,6 @@ public struct ScrollView: TypeSafeView, View { ) if willEarlyExit { - print("early exit: childResult.size=\(childResult.size)") return childResult } diff --git a/Sources/SwiftCrossUI/Views/VStack.swift b/Sources/SwiftCrossUI/Views/VStack.swift index f3917a2a4be..e757d932a80 100644 --- a/Sources/SwiftCrossUI/Views/VStack.swift +++ b/Sources/SwiftCrossUI/Views/VStack.swift @@ -50,7 +50,10 @@ public struct VStack: View { // TODO: Make layout caching a ViewGraphNode feature so that we can handle // these edge cases without a second thought. Would also make introducing // a port of SwiftUI's Layout protocol much easier. - print("warning: VStack will not function correctly non-TupleView Content") + logger.warning( + "HStack will not function correctly with non-TupleView content", + metadata: ["childrenType": "\(type(of: children))"] + ) } var cache = (children as? TupleViewChildren)?.stackLayoutCache ?? StackLayoutCache() let result = LayoutSystem.computeStackLayout( diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift index 94d785588a2..7cccf22b1c7 100644 --- a/Sources/SwiftCrossUI/_App.swift +++ b/Sources/SwiftCrossUI/_App.swift @@ -55,13 +55,11 @@ class _App { let mirror = Mirror(reflecting: self.app) for property in mirror.children { if property.label == "state" && property.value is ObservableObject { - print( + logger.warning( """ - - warning: The App.state protocol requirement has been removed in favour of - SwiftUI-style @State annotations. Decorate \(AppRoot.self).state - with the @State property wrapper to restore previous behaviour. - + the App.state protocol requirement has been removed in favour of \ + SwiftUI-style @State annotations; decorate \(AppRoot.self).state \ + with the @State property wrapper to restore previous behaviour """ ) } diff --git a/Sources/UIKitBackend/KeyboardToolbar.swift b/Sources/UIKitBackend/KeyboardToolbar.swift index 8a7d9e9c8df..ce62b427afe 100644 --- a/Sources/UIKitBackend/KeyboardToolbar.swift +++ b/Sources/UIKitBackend/KeyboardToolbar.swift @@ -107,11 +107,11 @@ extension Button: ToolbarItem { extension Spacer: ToolbarItem { public func createBarButtonItem() -> UIBarButtonItem { if let minLength, minLength > 0 { - print( + logger.warning( """ - Warning: Spacer's minLength property is ignored within keyboard toolbars \ - due to UIKit limitations. Use `Spacer()` for unconstrained spacers and \ - `Spacer().frame(width: _)` for fixed-length spacers. + Spacer's minLength property is ignored within keyboard toolbars \ + due to UIKit limitations; use `Spacer()` for unconstrained spacers and \ + `Spacer().frame(width: _)` for fixed-length spacers """ ) } diff --git a/Sources/UIKitBackend/UIKitBackend+Menu.swift b/Sources/UIKitBackend/UIKitBackend+Menu.swift index efe33affa9c..bb91344972b 100644 --- a/Sources/UIKitBackend/UIKitBackend+Menu.swift +++ b/Sources/UIKitBackend/UIKitBackend+Menu.swift @@ -66,7 +66,7 @@ extension UIKitBackend { #else // Once keyboard shortcuts are implemented, it might be possible to do them on more // platforms than just Mac Catalyst. For now, this is a no-op. - print("UIKitBackend: ignoring \(#function) call") + logger.notice("ignoring \(#function) call") #endif } } diff --git a/Sources/UIKitBackend/UIKitBackend+Path.swift b/Sources/UIKitBackend/UIKitBackend+Path.swift index 05d2ecfdd93..ac1e0ff9507 100644 --- a/Sources/UIKitBackend/UIKitBackend+Path.swift +++ b/Sources/UIKitBackend/UIKitBackend+Path.swift @@ -143,7 +143,10 @@ extension UIKitBackend { case .bevel: shapeLayer.lineJoin = .bevel @unknown default: - print("Warning: unrecognized lineJoinStyle \(path.lineJoinStyle)") + logger.warning( + "unrecognized lineJoinStyle", + metadata: ["lineJoinStyle": "\(path.lineJoinStyle)"] + ) shapeLayer.lineJoin = .miter } @@ -155,7 +158,10 @@ extension UIKitBackend { case .square: shapeLayer.lineCap = .square @unknown default: - print("Warning: unrecognized lineCapStyle \(path.lineCapStyle)") + logger.warning( + "unrecognized lineCapStyle", + metadata: ["lineCapStyle": "\(path.lineCapStyle)"] + ) shapeLayer.lineCap = .butt } diff --git a/Sources/UIKitBackend/UIKitBackend+Sheet.swift b/Sources/UIKitBackend/UIKitBackend+Sheet.swift index 9370394b7a4..81f9dd148a1 100644 --- a/Sources/UIKitBackend/UIKitBackend+Sheet.swift +++ b/Sources/UIKitBackend/UIKitBackend+Sheet.swift @@ -159,9 +159,9 @@ extension UIKitBackend { // TODO: Maybe we can backport the detent behaviour? debugLogOnce( """ - Your current OS version doesn't support variable sheet heights. \ - Setting presentationDetents only has an effect from iOS 15.0. \ - tvOS and visionOS do not support it at all. + your current OS version doesn't support variable sheet heights; \ + setting presentationDetents only has an effect from iOS 15.0; \ + tvOS and visionOS do not support it at all """ ) } @@ -177,9 +177,9 @@ extension UIKitBackend { } else { debugLogOnce( """ - Your current OS version doesn't support variable sheet corner \ - radii. Setting them only has an effect from iOS 15.0. tvOS and \ - visionOS do not support it at all. + your current OS version doesn't support variable sheet corner \ + radii; setting them only has an effect from iOS 15.0; tvOS and \ + visionOS do not support it at all """ ) } @@ -206,9 +206,9 @@ extension UIKitBackend { } else { debugLogOnce( """ - Your current OS version doesn't support setting sheet drag \ - indicator visibility. Setting this only has an effect from iOS \ - 15.0. tvOS and visionOS do not support it at all. + your current OS version doesn't support setting sheet drag \ + indicator visibility; setting this only has an effect from iOS \ + 15.0; tvOS and visionOS do not support it at all """ ) } diff --git a/Sources/UIKitBackend/UIKitBackend+WebView.swift b/Sources/UIKitBackend/UIKitBackend+WebView.swift index ae5214401d9..5c0bfd07d17 100644 --- a/Sources/UIKitBackend/UIKitBackend+WebView.swift +++ b/Sources/UIKitBackend/UIKitBackend+WebView.swift @@ -36,7 +36,7 @@ import SwiftCrossUI func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { guard let url = webView.url else { - print("warning: Web view has no URL") + logger.warning("web view has no URL") return } diff --git a/Sources/UIKitBackend/UIKitBackend+Window.swift b/Sources/UIKitBackend/UIKitBackend+Window.swift index 46aa666ee71..720a2deef81 100644 --- a/Sources/UIKitBackend/UIKitBackend+Window.swift +++ b/Sources/UIKitBackend/UIKitBackend+Window.swift @@ -130,15 +130,19 @@ extension UIKitBackend { } public func setResizability(ofWindow window: Window, to resizable: Bool) { - print("UIKitBackend: ignoring \(#function) call") + logger.notice("ignoring \(#function) call") } public func setSize(ofWindow window: Window, to newSize: SIMD2) { #if os(visionOS) window.bounds.size = CGSize(width: CGFloat(newSize.x), height: CGFloat(newSize.y)) #else - print( - "UIKitBackend: ignoring \(#function) call. Current window size: \(window.bounds.width) x \(window.bounds.height); proposed size: \(newSize.x) x \(newSize.y)" + logger.notice( + "ignoring \(#function) call", + metadata: [ + "currentWindowSize": "\(window.bounds.width) x \(window.bounds.height)", + "proposedWindowSize": "\(newSize.x) x \(newSize.y)", + ] ) #endif } diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index bb7010556be..34dff83ec0c 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -1,6 +1,20 @@ +import Logging import SwiftCrossUI import UIKit +/// The storage behind `logger`. +/// +/// `nil` if the logger hasn't been set yet. +/// +/// > Safety: This is only set once, before it is ever read. +nonisolated(unsafe) private var _logger: Logger? + +/// The global logger for this backend. +var logger: Logger { + guard let _logger else { fatalError("logger not yet initialized") } + return _logger +} + public final class UIKitBackend: AppBackend { static var onBecomeActive: (() -> Void)? static var onReceiveURL: ((URL) -> Void)? @@ -69,7 +83,7 @@ public final class UIKitBackend: AppBackend { #if DEBUG let location = LogLocation(file: file, line: line, column: column) if logsPerformed.insert(location).inserted { - print(message) + logger.notice("\(message)") } #endif } @@ -163,7 +177,11 @@ extension App { public typealias Backend = UIKitBackend public var backend: UIKitBackend { - UIKitBackend() + _logger = Logger( + label: "UIKitBackend", + factory: Self.logHandler(label:metadataProvider:) + ) + return UIKitBackend() } } diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 506db77b924..295c6a641ff 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1,5 +1,6 @@ import CWinRT import Foundation +import Logging import SwiftCrossUI import UWP import WinAppSDK @@ -8,6 +9,19 @@ import WinUI import WinUIInterop import WindowsFoundation +/// The storage behind `logger`. +/// +/// `nil` if the logger hasn't been set yet. +/// +/// > Safety: This is only set once, before it is ever read. +nonisolated(unsafe) private var _logger: Logger? + +/// The global logger for this backend. +var logger: Logger { + guard let _logger else { fatalError("logger not yet initialized") } + return _logger +} + // Many force tries are required for the WinUI backend but we don't really want them // anywhere else so just disable them for this file. // swiftlint:disable force_try @@ -16,7 +30,11 @@ extension App { public typealias Backend = WinUIBackend public var backend: WinUIBackend { - WinUIBackend() + _logger = Logger( + label: "WinUIBackend", + factory: Self.logHandler(label:metadataProvider:) + ) + return WinUIBackend() } } @@ -90,7 +108,10 @@ public final class WinUIBackend: AppBackend { // won't get seen anyway. But I don't trust my Windows knowledge enough // to assert that it's impossible to view logs on failure, so let's // print a warning anyway. - print("Warning: Failed to attach to parent console: \(error.localizedDescription)") + logger.warning( + "failed to attach to parent console", + metadata: ["error": "\(error)"] + ) } // Ensure that the app's windows adapt to DPI changes at runtime @@ -351,7 +372,7 @@ public final class WinUIBackend: AppBackend { public func setPosition(ofChildAt index: Int, in container: Widget, to position: SIMD2) { let container = container as! Canvas guard let child = container.children.getAt(UInt32(index)) else { - print("warning: child to set position of not found") + logger.warning("child to set position of not found") return } @@ -369,7 +390,7 @@ public final class WinUIBackend: AppBackend { } } - print("warning: child to remove not found") + logger.warning("child to remove not found") } public func createColorableRectangle() -> Widget { @@ -1181,7 +1202,7 @@ public final class WinUIBackend: AppBackend { func handleToggle() { if isChecked == nil { - print("warning: Checkbox in limbo") + logger.warning("checkbox in limbo") } onToggle?(isChecked ?? false) } @@ -1262,7 +1283,7 @@ public final class WinUIBackend: AppBackend { // WinUI only allows one dialog at a time so we limit ourselves using // a semaphore. guard let window = window ?? windows.first else { - print("warning: WinUI can't show alert without window") + logger.warning("WinUI can't show alert without window") return } @@ -1931,7 +1952,7 @@ public class CustomWindow: WinUI.Window { if result == S_OK { windowScaleFactor = Double(x) / Double(USER_DEFAULT_SCREEN_DPI) } else { - print("Warning: Failed to get window scale factor, defaulting to 1.0") + logger.warning("failed to get window scale factor, defaulting to 1.0") windowScaleFactor = 1 }