Back to catalog

swiftui-converter

Convert AppKit/NSView-based graphics and UI code to modern SwiftUI

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
description: Convert AppKit/NSView-based graphics and UI code to modern SwiftUI
---

# SwiftUI Converter

Convert AppKit/NSView-based graphics and UI code to modern SwiftUI.

## Capabilities

1. **View Conversion**
   - Convert NSView subclasses to SwiftUI Views
   - Migrate NSViewController to SwiftUI view hierarchies
   - Transform IBOutlets/IBActions to @State/@Binding
   - Convert NIB/XIB layouts to SwiftUI declarative syntax

2. **Graphics Migration**
   - Convert NSBezierPath to SwiftUI Path
   - Transform custom drawing code to Canvas/Shape
   - Migrate CoreGraphics drawing to SwiftUI primitives
   - Convert CALayer animations to SwiftUI animations

3. **Data Flow Modernization**
   - Replace delegate patterns with @Binding/@Environment
   - Convert KVO to @Published properties
   - Migrate NSNotificationCenter to Combine publishers
   - Transform target-action to SwiftUI button actions

4. **Layout System**
   - Convert Auto Layout constraints to SwiftUI layout
   - Transform frame-based layouts to GeometryReader
   - Migrate NSStackView to HStack/VStack/ZStack
   - Convert NSScrollView to ScrollView

5. **macOS Specific Features**
   - Convert NSWindowController to Window/WindowGroup
   - Migrate NSToolbar to SwiftUI toolbar modifiers
   - Transform NSMenu to SwiftUI CommandGroup
   - Convert NSAlert to SwiftUI alert modifiers

## Workflow

When invoked, this skill will:

1. **Analyze**: Identify AppKit views and controllers to convert
2. **Plan**: Generate conversion strategy with dependencies
3. **Convert**: Transform code to SwiftUI equivalents
4. **Integrate**: Update parent views and data flow
5. **Test**: Verify behavior matches original

## Usage Instructions

When the user invokes this skill:

1. Ask what to convert:
   - Specific view/controller
   - Entire view hierarchy
   - Graphics rendering code only
   - UI controls and layout

2. Ask conversion approach:
   - **Incremental**: Wrap AppKit views in NSViewRepresentable
   - **Full Rewrite**: Pure SwiftUI implementation
   - **Hybrid**: SwiftUI shell with AppKit components

3. Analyze dependencies and order
4. Generate SwiftUI code
5. Update data flow and bindings

## Project-Specific Context

### Your Graphics Classes

Based on your codebase:
- `Graphic` base class with NSBezierPath rendering
- `GraphicPetal`, `GraphicCircle`, `GraphicKite`, etc.
- Custom geometry calculations
- AppKit-based drawing

### Example Conversion

#### Before: AppKit Graphics (GraphicPetal)
```swift
@objc class GraphicPetal: Graphic {
    override var lineWidth: Float {
        didSet { drawingPath?.lineWidth = CGFloat(lineWidth) }
    }

    override var drawingPath: NSBezierPath? {
        didSet { drawingPath?.lineWidth = CGFloat(lineWidth) }
    }

    private var petalIncrement: Int = 0
    private var maxRadius: Float = 0.0
    private var percent: Float = 0.0

    @objc override func calculateGeometry() {
        // Complex geometry calculation
        let path = NSBezierPath()
        // ... drawing code
        drawingPath = path
    }

    @objc func draw() {
        strokeColor.setStroke()
        fillColor.setFill()
        drawingPath?.stroke()
        if drawsFill {
            drawingPath?.fill()
        }
    }
}
```

#### After: SwiftUI Graphics
```swift
struct PetalShape: Shape {
    let petalIncrement: Int
    let maxRadius: CGFloat
    let percent: CGFloat
    let geometryController: GraphicGeometrySource

    func path(in rect: CGRect) -> Path {
        var path = Path()

        // Translate geometry calculation to Path
        let angles = calculateAngles(
            petalIncrement: petalIncrement,
            size: geometryController.petalSize,
            start: geometryController.startAngle
        )

        // Build path using SwiftUI Path API
        path.move(to: center)
        path.addLine(to: outerPoint)
        path.addArc(...)

        return path
    }

    private func calculateAngles(...) -> [CGFloat] {
        // Same geometry logic, adapted
    }
}

// Usage
struct PetalView: View {
    let petalIncrement: Int
    let maxRadius: CGFloat
    let percent: CGFloat
    let strokeColor: Color
    let fillColor: Color
    let lineWidth: CGFloat

    var body: some View {
        PetalShape(
            petalIncrement: petalIncrement,
            maxRadius: maxRadius,
            percent: percent,
            geometryController: geometryController
        )
        .stroke(strokeColor, lineWidth: lineWidth)
        .background(
            PetalShape(...)
                .fill(fillColor)
        )
    }
}
```

## Common Conversion Patterns

### 1. NSBezierPath → SwiftUI Path

```swift
// AppKit
let bezierPath = NSBezierPath()
bezierPath.move(to: NSPoint(x: 10, y: 10))
bezierPath.line(to: NSPoint(x: 100, y: 100))
bezierPath.close()

// SwiftUI
var path = Path()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 100, y: 100))
path.closeSubpath()
```

### 2. Custom NSView Drawing → Canvas

```swift
// AppKit
class CustomView: NSView {
    override func draw(_ dirtyRect: NSRect) {
        NSColor.red.setFill()
        dirtyRect.fill()

        let path = NSBezierPath(ovalIn: bounds)
        NSColor.blue.setStroke()
        path.stroke()
    }
}

// SwiftUI
struct CustomView: View {
    var body: some View {
        Canvas { context, size in
            // Fill background
            context.fill(
                Path(CGRect(origin: .zero, size: size)),
                with: .color(.red)
            )

            // Draw circle
            let circlePath = Path(
                ellipseIn: CGRect(origin: .zero, size: size)
            )
            context.stroke(
                circlePath,
                with: .color(.blue)
            )
        }
    }
}
```

### 3. NSViewController → SwiftUI View

```swift
// AppKit
class SettingsViewController: NSViewController {
    @IBOutlet weak var nameField: NSTextField!
    @IBOutlet weak var enabledCheckbox: NSButton!

    @IBAction func saveSettings(_ sender: Any) {
        UserDefaults.standard.set(nameField.stringValue, forKey: "name")
        UserDefaults.standard.set(
            enabledCheckbox.state == .on,
            forKey: "enabled"
        )
    }
}

// SwiftUI
struct SettingsView: View {
    @AppStorage("name") private var name = ""
    @AppStorage("enabled") private var enabled = false

    var body: some View {
        Form {
            TextField("Name", text: $name)
            Toggle("Enabled", isOn: $enabled)
        }
        .formStyle(.grouped)
    }
}
```

### 4. Delegate Pattern → @Binding

```swift
// AppKit
protocol ColorPickerDelegate: AnyObject {
    func colorPicker(_ picker: ColorPicker, didSelect color: NSColor)
}

class ColorPicker: NSView {
    weak var delegate: ColorPickerDelegate?

    func selectColor(_ color: NSColor) {
        delegate?.colorPicker(self, didSelect: color)
    }
}

// SwiftUI
struct ColorPicker: View {
    @Binding var selectedColor: Color

    var body: some View {
        ColorPicker("Color", selection: $selectedColor)
            .onChange(of: selectedColor) { newColor in
                // Handle change if needed
            }
    }
}
```

### 5. NSWindowController → WindowGroup

```swift
// AppKit
class DocumentWindowController: NSWindowController {
    static func create() -> DocumentWindowController {
        let controller = DocumentWindowController(
            windowNibName: "DocumentWindow"
        )
        controller.showWindow(nil)
        return controller
    }
}

// SwiftUI
@main
struct PaleoRoseApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: PaleoRoseDocument()) { file in
            DocumentView(document: file.$document)
        }
        .commands {
            // Custom menu commands
        }
    }
}
```

## Graphics Migration Strategy

### For Your Rose Diagram Graphics

Your project has specialized graphics (petals, kites, histograms). Recommended approach:

1. **Keep Geometry Logic**
   - Extract calculation methods
   - Make them pure functions
   - Reuse in SwiftUI Shapes

2. **Create Shape Protocols**
```swift
protocol RoseDiagramShape {
    var geometryController: GraphicGeometrySource { get }
    func calculatePath() -> Path
}
```

3. **Incremental Migration**
   - Start with simple shapes (Circle, Line)
   - Move to complex shapes (Petal, Kite)
   - Keep AppKit version until verified

4. **Hybrid Approach**
```swift
// Wrap AppKit view temporarily
struct LegacyGraphicView: NSViewRepresentable {
    let graphic: Graphic

    func makeNSView(context: Context) -> NSView {
        let view = GraphicHostView()
        view.graphic = graphic
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {
        (nsView as? GraphicHostView)?.graphic = graphic
    }
}
```

## AppKit-to-SwiftUI Equivalents

| AppKit | SwiftUI |
|--------|---------|
| NSView | View |
| NSViewController | View |
| NSWindowController | Window/WindowGroup |
| NSButton | Button |
| NSTextField | TextField |
| NSTextView | TextEditor |
| NSImageView | Image |
| NSScrollView | ScrollView |
| NSStackView | HStack/VStack |
| NSTableView | List/Table |
| NSOutlineView | OutlineGroup |
| NSMenu | Menu |
| NSToolbar | .toolbar { } |
| NSAlert | .alert { } |
| NSColorWell | ColorPicker |
| NSSlider | Slider |
| NSProgressIndicator | ProgressView |
| NSBezierPath | Path |
| CALayer | Canvas/Shape |

## Data Flow Migration

### Observable Pattern
```swift
// AppKit (KVO)
@objc dynamic var name: String = ""

// SwiftUI
@Observable
class Model {
    var name: String = ""
}
```

### Notification Center
```swift
// AppKit
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .didChangeValue,
    object: nil
)

// SwiftUI
struct MyView: View {
    var body: some View {
        Text("Hello")
            .onReceive(NotificationCenter.default.publisher(
                for: .didChangeValue
            )) { _ in
                handleChange()
            }
    }
}
```

## Testing Strategy

1. **Visual Comparison**
   - Screenshot AppKit version
   - Render SwiftUI version
   - Compare pixel-by-pixel

2. **Behavioral Tests**
   - Same user interactions
   - Same state changes
   - Same data updates

3. **Performance Benchmarks**
   - Measure rendering time
   - Check memory usage
   - Profile animation smoothness

## Incremental Adoption Path

For PaleoRose migration:

1. **Phase 1: New Windows**
   - Settings window (already SwiftUI)
   - About window (already SwiftUI)

2. **Phase 2: Simple Views**
   - Inspector panels
   - Toolbars

3. **Phase 3: Complex Graphics**
   - Rose diagram rendering
   - Interactive layers

4. **Phase 4: Main Document**
   - Full document window
   - Integration with existing model

## Configuration

Store conversion preferences in `.swiftui-converter.json`:
```json
{
  "targetMinimumOS": "macOS 14.0",
  "conversionStyle": "incremental",
  "keepAppKitFallbacks": true,
  "generatePreviews": true,
  "useNewObservablePattern": true
}
```