Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed1039b
adopt swift-log
kaascevich Dec 29, 2025
439952b
undo a test change I made
kaascevich Dec 29, 2025
378ce3a
documentation and formatting fixups
kaascevich Dec 29, 2025
f5bf14b
cache logs by `extractSwiftBundlerMetadata()` and output them once th…
kaascevich Dec 29, 2025
1c9cee8
add a `logHandler(label:metadata:)` requirement to `App` and use it
kaascevich Dec 29, 2025
cca959a
use swift-log in the backends
kaascevich Dec 29, 2025
ca62da6
use `InMemoryLogHandler` for `extractSwiftBundlerMetadata()`
kaascevich Dec 30, 2025
ccfba39
refactor `extractSwiftBundlerMetadata()` to be `throws`
kaascevich Dec 30, 2025
be915af
documentation fixups
kaascevich Dec 30, 2025
5d0bdfa
fix comment casing
kaascevich Dec 30, 2025
150a6cd
fix swift-bundler metadata error log not using `localizedDescription`
kaascevich Dec 30, 2025
41b3a37
Merge branch 'main' into swift-log
kaascevich Dec 31, 2025
87582a2
convert remaining `print` calls into `logger` calls
kaascevich Dec 31, 2025
8e6200e
remove `logger.notice` call only in place for debugging
kaascevich Jan 2, 2026
d494666
change the minimum swift-log version to 1.7.0 (for Swift 5.10 compat)
kaascevich Jan 2, 2026
9837f5e
lower swift-log version again
kaascevich Jan 2, 2026
5d28af8
Merge branch 'stackotter:main' into swift-log
kaascevich Jan 2, 2026
dc3c131
add missing `return` to UIKitBackend init
kaascevich Jan 2, 2026
cf457ff
please just work this time
kaascevich Jan 2, 2026
7424008
commit Package.resolved properly
kaascevich Jan 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
from: "1.8.0"
),
// .package(
// url: "https://github.com/stackotter/TermKit",
// revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704"
Expand All @@ -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",
Expand Down
50 changes: 43 additions & 7 deletions Sources/SwiftCrossUI/App.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
import Foundation
import Logging

/// The global logger.
///
/// `nil` if the logger hasn't been set yet (which is only the case during
/// ``App/init()``).
///
/// > Safety: This is only ever set in one place, namely in ``App/main()``.
private nonisolated(unsafe) var _logger: Logger?

/// The global logger.
///
/// This is safe to use from anywhere in the library (except in
/// `extractSwiftBundlerMetadata()`, since that's called before the logger is
/// initialized -- this property will fatal-error in that case).
var logger: Logger {
guard let _logger else { fatalError("logger not yet initialized") }
return _logger
}

/// An application.
@MainActor
Expand All @@ -8,9 +27,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.
Expand All @@ -20,6 +41,9 @@ 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 [setting the logging backend](doc:Logging).
init()
}

Expand All @@ -28,7 +52,9 @@ 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 is accessible from within ``App/init()``.
@MainActor
private var swiftBundlerAppMetadata: AppMetadata?

Expand All @@ -52,6 +78,8 @@ private enum SwiftBundlerMetadataError: LocalizedError {

extension App {
/// Metadata loaded at app start up.
///
/// This is accessible from within ``init()``.
public static var metadata: AppMetadata? {
swiftBundlerAppMetadata
}
Expand All @@ -61,6 +89,11 @@ extension App {
swiftBundlerAppMetadata = extractSwiftBundlerMetadata()

let app = Self()

// set up the logger _after_ calling App's initializer; that way users
// can call LoggingSystem.bootstrap in the init
_logger = Logger(label: "SwiftCrossUI")

let _app = _App(app)
_forceRefresh = {
app.backend.runInMainThread {
Expand All @@ -71,13 +104,16 @@ extension App {
}

private static func extractSwiftBundlerMetadata() -> AppMetadata? {
// NB: the logger hasn't been set up yet when this is called, so we're
// forced to rely on good ol' `print`

guard let executable = Bundle.main.executableURL else {
print("warning: No executable url")
print("warning: no executable url")
return nil
}

guard let data = try? Data(contentsOf: executable) else {
print("warning: Executable failed to read self (to extract metadata)")
print("warning: executable failed to read itself (to extract metadata)")
return nil
}

Expand Down Expand Up @@ -115,7 +151,7 @@ extension App {
additionalMetadata: additionalMetadata
)
} catch {
print("warning: Swift Bundler metadata present but couldn't be parsed")
print("warning: swift-bundler metadata present but couldn't be parsed")
print(" -> \(error)")
return nil
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -801,15 +801,15 @@ 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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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
})
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/SwiftCrossUI/Environment/Actions/OpenURLAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
])
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
])
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,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
Expand Down
10 changes: 5 additions & 5 deletions Sources/SwiftCrossUI/Layout/LayoutSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ public enum LayoutSystem {
dryRun: dryRun
)
if result.participatesInStackLayouts {
print(
"""
warning: Hidden view became visible on second update. \
Layout may break. View: \(child.tag ?? "<unknown type>")
"""
logger.warning(
"hidden view became visible on second update; layout may break",
metadata: [
"view": "\(child.tag ?? "<unknown type>")"
]
)
}
renderedChildren[index] = result
Expand Down
18 changes: 10 additions & 8 deletions Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,16 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
// 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
broken (or both).
-> dryRunResult.size: \(dryRunResult.size)
-> finalContentResult.size: \(finalContentResult.size)
"""
logger.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 \
broken (or both)
""",
metadata: [
"dryRunResult.size": "\(dryRunResult.size)",
"finalContentResult.size": "\(finalContentResult.size)",
]
)

// Give the view graph one more chance to sort itself out to fail
Expand Down
17 changes: 17 additions & 0 deletions Sources/SwiftCrossUI/SwiftCrossUI.docc/Logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 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 swift-log backend you want.

To do so, add your backend of choice to your package dependencies, then call
`LoggingSystem.bootstrap(_:)` in your ``App``'s initializer. This initializer
is called before anything else happens, so you can also perform other early
setup tasks in here.

> Important: Calling `LoggingSystem.bootstrap(_:)` anywhere outside of
> ``App/init()`` will not correctly set the logging backend for SwiftCrossUI, as
> the library's internal logger is set up immediately after the initializer
> returns.
1 change: 1 addition & 0 deletions Sources/SwiftCrossUI/SwiftCrossUI.docc/SwiftCrossUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ SwiftCrossUI takes inspiration from SwiftUI, allowing you to use the basic conce

### Other

- <doc:Logging>
- <doc:Implementation-details>
- <doc:Deprecated>
9 changes: 2 additions & 7 deletions Sources/SwiftCrossUI/Values/TextStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
23 changes: 10 additions & 13 deletions Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,11 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend>: Sendable {
let mirror = Mirror(reflecting: view)
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
"""
)
}
Expand Down Expand Up @@ -154,13 +152,12 @@ public class ViewGraphNode<NodeView: View, Backend: AppBackend>: Sendable {
dryRun: false
)
if finalResult.size != newResult.size {
print(
"""
warning: State-triggered view update had mismatch \
between dry-run size and final size.
-> dry-run size: \(newResult.size)
-> final size: \(finalResult.size)
"""
logger.warning(
"state-triggered view update had mismatch between dry-run size and final size",
metadata: [
"dry-run size": "\(newResult.size)",
"final size": "\(finalResult.size)",
]
)
}
self.currentResult = finalResult
Expand Down
10 changes: 4 additions & 6 deletions Sources/SwiftCrossUI/_App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,11 @@ class _App<AppRoot: 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
"""
)
}
Expand Down