# Swift

Most of AppKit needs no Swift: Rust calls it through objc2, and the [map widget](https://nativedesktop.formalsnake.dev/concepts/native-widgets.md)
is written that way. Add Swift only when the control you want exists in Swift or SwiftUI alone,
such as SwiftUI's `Gauge` or Swift Charts, or when it is a SwiftUI control Apple restyles with
each OS release, so hosting it keeps the app current. The rest of the app stays Rust, and off
macOS the widget's drawn version is used.

The Places example has a circular gauge written in SwiftUI and shown through `NSHostingView`.

> **Example: Places.** Source in [examples/native-widget](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget). Live on the web and captured on macOS, Linux and Windows in the docs app.


## Wire it up

Three pieces connect an app's Swift package to the Rust build.

1. List the package in `nativedesktop.toml`:

```toml
[macos]
swift_packages = ["swift/Widgets"]
minimum_system_version = "14.0"
entitlements = ["network.client"]   # map tiles
```

<sub>From [examples/native-widget/nativedesktop.toml](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/nativedesktop.toml), line 8.</sub>


2. Call `nd::build::app()` from `build.rs`, with `nd` as a build dependency using the `build`
   feature. `nd new` writes this line into every template. On other targets it does nothing.

```rust
nd::build::app();
```

<sub>From [examples/native-widget/build.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/build.rs), line 5.</sub>


3. Register the Swift view next to the drawn one with `Backends::swift`:

```rust
pub struct GaugeWidget;

impl NativeWidget for GaugeWidget {
    type Props = GaugeProps;
    type Event = Infallible;

    fn semantics(props: &GaugeProps) -> Semantics {
        Semantics::new(Role::ProgressIndicator).label(props.label.clone()).value(SemanticValue::range(
            props.value,
            props.min,
            props.max,
        ))
    }

    fn drawn(props: &GaugeProps, _emit: Emit<Infallible>, _window: &mut Window, cx: &mut App) -> AnyElement {
        let theme = cx.theme();
        let fraction = ((props.value - props.min) / (props.max - props.min)).clamp(0.0, 1.0) as f32;
        let (track, fill) = (theme.border.subtle, theme.accent);
        v_flex()
            .items_center()
            .gap_1()
            .child(
                div()
                    .relative()
                    .size(px(48.))
                    .flex()
                    .items_center()
                    .justify_center()
                    .child(
                        canvas(move |bounds, paint, _cx| {
                            let radius = bounds.size.width.min(bounds.size.height) / 2. - px(3.);
                            paint.stroke_arc(bounds.center(), radius, 0.0, 1.0, px(5.), track);
                            paint.stroke_arc(bounds.center(), radius, 0.0, fraction, px(5.), fill);
                        })
                        .absolute()
                        .inset_0(),
                    )
                    .child(text(format!("{:.0}", props.value)).font_weight(FontWeight::SEMIBOLD).tabular_nums()),
            )
            .child(text(props.label.clone()).text_size(theme.type_scale.caption).text_color(theme.text.secondary))
            .into_any_element()
    }

    fn backends() -> Backends<Self> {
        let backends = Backends::drawn_only();
        #[cfg(target_os = "macos")]
        let backends = backends.swift(SwiftWidget::named("Gauge"));
        backends
    }
}
```

<sub>From [examples/native-widget/src/gauge/mod.rs](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/src/gauge/mod.rs), line 19.</sub>


`nd::build::app()` collects the framework's Swift packages and the app's own, generates one
aggregate SwiftPM package in `OUT_DIR`, builds one static library and links it into the binary.
Nothing is written into your source tree. The app's `Package.swift` declares a plain static
library with no dependencies; the script passes the framework's own package (`NDSwift`) to the
aggregate through `ND_SWIFT_PATH`.

## The Swift view

The Swift side exports C functions with `@_cdecl`, the same shape the framework's own Swift
uses: `create` returns a +1 retained view built from JSON props, `update` applies new props and
`measure` reports the fitting size. Props cross as JSON, which is fine because a widget's props
cross only when they change. `GaugeProps` has the same field names on both sides, and the Rust
props derive `serde::Serialize`, which `Backends::swift` requires.

The name given to `SwiftWidget::named` picks the symbols: `"Gauge"` is `nd_widget_gauge_create`,
`nd_widget_gauge_update` and `nd_widget_gauge_measure`, and a name of several words joins them
with underscores (`"MapView"` is `nd_widget_map_view_create`). The AppKit backend looks the
three up in the running binary the first time the widget mounts. When they are missing, because
the package is not in `swift_packages` or `build.rs` does not call `nd::build::app()`, it prints
the symbol it wanted and draws the widget instead.

```swift
struct GaugeBody: View {
    let props: GaugeProps

    var body: some View {
        Gauge(value: props.value, in: props.min...props.max) {
            Text(props.label)
        } currentValueLabel: {
            Text(props.value, format: .number.precision(.fractionLength(0)))
        }
        .gaugeStyle(.accessoryCircularCapacity)
    }
}

private func decode(_ json: UnsafePointer<CChar>) -> GaugeProps {
    let data = Data(String(cString: json).utf8)
    return (try? JSONDecoder().decode(GaugeProps.self, from: data))
        ?? GaugeProps(value: 0, min: 0, max: 100, label: "")
}

@MainActor
private func hosting(_ view: UnsafeMutableRawPointer) -> NSHostingView<GaugeBody> {
    Unmanaged<NSHostingView<GaugeBody>>.fromOpaque(view).takeUnretainedValue()
}

/// Returns a +1 retained `NSHostingView`; the caller releases it.
@MainActor @_cdecl("nd_widget_gauge_create")
public func placesGaugeCreate(_ props: UnsafePointer<CChar>) -> UnsafeMutableRawPointer {
    Unmanaged.passRetained(NSHostingView(rootView: GaugeBody(props: decode(props)))).toOpaque()
}

@MainActor @_cdecl("nd_widget_gauge_update")
public func placesGaugeUpdate(_ view: UnsafeMutableRawPointer, _ props: UnsafePointer<CChar>) {
    hosting(view).rootView = GaugeBody(props: decode(props))
}

@MainActor @_cdecl("nd_widget_gauge_measure")
public func placesGaugeMeasure(
    _ view: UnsafeMutableRawPointer, _ width: UnsafeMutablePointer<Double>,
    _ height: UnsafeMutablePointer<Double>
) {
    let size = hosting(view).fittingSize
    width.pointee = size.width
    height.pointee = size.height
}
```

<sub>From [examples/native-widget/swift/Widgets/Sources/Widgets/Gauge.swift](https://github.com/FormalSnake/NativeDesktop/blob/main/examples/native-widget/swift/Widgets/Sources/Widgets/Gauge.swift), line 14.</sub>


Pass universal state (enabled, tooltip, focus) into SwiftUI state yourself: a hosting view is not
the control the user sees.

Events do not cross from Swift yet: the C functions take no emitter, so a Swift widget's
`Event` is `Infallible`. A widget that reports user input writes its AppKit side in objc2, like
the map.

## Toolchain

Swift and the macOS SDK come from the system Xcode or Command Line Tools, never from Nix. The
minimum versions are pinned in [swift-toolchain.toml](https://github.com/FormalSnake/NativeDesktop/blob/main/swift-toolchain.toml) (Swift 6, SDK
26). `nd::build::app()` fails with a message when the installed majors are lower.
The Swift runtime is the OS's own in `/usr/lib/swift`, present from macOS 14, so the example
declares `minimum_system_version = "14.0"`.

Inside the Nix dev shell the build resolves the SDK with `xcrun --sdk macosx --show-sdk-path` and
links with `/usr/bin/clang`, so no `SDKROOT` needs setting.

## When not to use Swift

- A control AppKit already has (`NSButton`, `NSTableView`, `MKMapView`): use objc2. It has no
  second toolchain and the call goes straight from Rust.
- A look you can compose from elements: write a composed widget, which runs on every OS.
- Anything that must also run on Linux or Windows: write the drawn version anyway. Swift only
  adds the macOS implementation.
