PureXML/third_party/rules-swift/exp/app-target.md

11 KiB

App Target Structure

Three patterns for the app entry point: SwiftUI (minimal App shell), UIKit (SceneDelegate), AppKit (NSApplicationDelegate).

Reference layouts:

  • SwiftUI pattern: Apps/iosApp/, Apps/macApp/ (one Xcode project per platform under Apps/)
  • UIKit/AppKit pattern: Apps/App/UIKitApp/, Apps/App/AppKitApp/ (siblings under a single project)

Pattern 1: SwiftUI Apps (Minimal App Shell)

SwiftUI app targets MUST be minimal shells that import a feature target's module and display a view.

iOS App Target (iosApp or iOSApp)

// Apps/iosApp/iosApp/iosAppApp.swift
import AppFeature  // ← Import the feature target's module
import SwiftUI

@main
struct iosAppApp: App {
    var body: some Scene {
        WindowGroup {
            AppView()  // ← Use the view from the feature target
        }
    }
}

macOS App Target (macApp)

// Apps/macApp/macApp/macAppApp.swift
import AppFeature  // ← Same feature target as iOS
import SwiftUI

@main
struct macAppApp: App {
    var body: some Scene {
        WindowGroup {
            AppView()  // ← Same view as iOS
        }
    }
}

Directory Structure:

Apps/
├── iosApp/
│   └── iosApp/
│       ├── iosAppApp.swift        # Minimal: import + AppView()
│       └── Assets.xcassets/
├── macApp/
│   └── macApp/
│       ├── macAppApp.swift        # Minimal: import + AppView()
│       └── Assets.xcassets/

Rules:

  1. App targets contain only the @main struct, import AppFeature, and AppView().
  2. All UI logic lives in the feature target (e.g. AppFeature).
  3. Both iOS and macOS app targets use the same feature target.
  4. The feature target exports AppView (a SwiftUI View).
  5. Font registration (if needed) goes in init() before body

Font Registration in SwiftUI App

// Apps/iosApp/iosApp/iosAppApp.swift
import AppFeature
import AppFont  // ← Import font target's module
import SwiftUI

@main
struct iosAppApp: App {
    init() {
        FontRegistration.registerFonts()  // ← Register before UI
    }

    var body: some Scene {
        WindowGroup {
            AppView()
        }
    }
}

Pattern 2: UIKit Apps (iOS - SceneDelegate Pattern)

UIKit app targets MUST delegate to a feature target for window creation.

Reference layout: Apps/App/UIKitApp/

AppDelegate.swift

// Apps/UIKitApp/AppDelegate.swift
import UIKit

@main
@MainActor
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        true
    }

    // MARK: UISceneSession Lifecycle

    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
}

SceneDelegate.swift

// Apps/UIKitApp/SceneDelegate.swift
import CanvasFeature  // ← Import the feature target's module (NOT "Canvas")
import UIKit

@MainActor
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = scene as? UIWindowScene else { return }
        window = CanvasFeature.createMainWindow(for: windowScene)  // ← Factory from feature
    }
}

Directory Structure:

Apps/
└── UIKitApp/
    ├── AppDelegate.swift     # Minimal lifecycle
    ├── SceneDelegate.swift   # Import CanvasFeature, create window
    ├── Info.plist
    └── Assets.xcassets/

Rules:

  1. AppDelegate handles only app lifecycle (minimal code).
  2. SceneDelegate imports the feature target's module.
  3. The feature target provides createMainWindow(for:) factory function.
  4. The feature target name ends with Feature (e.g. CanvasFeature, not Canvas).

Pattern 3: AppKit Apps (macOS - NSApplicationDelegate Pattern)

AppKit app targets MUST delegate to a feature target, with menu code in a SEPARATE file.

Reference layout: Apps/App/AppKitApp/

main.swift

// Apps/AppKitApp/main.swift
import Cocoa

MainActor.assumeIsolated {
    let delegate = AppDelegate()
    NSApplication.shared.delegate = delegate
}
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

AppDelegate.swift

// Apps/AppKitApp/AppDelegate.swift
import CanvasFeature  // ← Import the feature target's module (NOT "Canvas")
import Cocoa

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
    var windowController: NSWindowController?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        setupMainMenu()  // ← Separate file for menus
        windowController = CanvasFeature.createWindowController()  // ← Factory from feature
        windowController?.window?.makeKeyAndOrderFront(nil)
        NSApp.activate(ignoringOtherApps: true)
    }

    func applicationWillTerminate(_ aNotification: Notification) {}

    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
        true
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        true
    }
}

MainMenu.swift (SEPARATE FILE)

// Apps/AppKitApp/MainMenu.swift
import Cocoa

extension AppDelegate {
    func setupMainMenu() {
        let mainMenu = NSMenu()

        // App menu
        let appMenu = NSMenu()
        appMenu.addItem(NSMenuItem(title: "About MyApp", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
        appMenu.addItem(.separator())
        appMenu.addItem(NSMenuItem(title: "Hide MyApp", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h"))
        let hideOthersItem = NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
        hideOthersItem.keyEquivalentModifierMask = [.command, .option]
        appMenu.addItem(hideOthersItem)
        appMenu.addItem(NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: ""))
        appMenu.addItem(.separator())
        appMenu.addItem(NSMenuItem(title: "Quit MyApp", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))

        let appMenuItem = NSMenuItem()
        appMenuItem.submenu = appMenu
        mainMenu.addItem(appMenuItem)

        // File menu
        let fileMenu = NSMenu(title: "File")
        fileMenu.addItem(NSMenuItem(title: "New", action: #selector(newDocument(_:)), keyEquivalent: "n"))
        fileMenu.addItem(NSMenuItem(title: "Open...", action: #selector(openDocument(_:)), keyEquivalent: "o"))
        fileMenu.addItem(.separator())
        fileMenu.addItem(NSMenuItem(title: "Close Window", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w"))

        let fileMenuItem = NSMenuItem()
        fileMenuItem.submenu = fileMenu
        mainMenu.addItem(fileMenuItem)

        // Edit menu
        let editMenu = NSMenu(title: "Edit")
        editMenu.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"))
        editMenu.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"))
        editMenu.addItem(.separator())
        editMenu.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
        editMenu.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
        editMenu.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
        editMenu.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))

        let editMenuItem = NSMenuItem()
        editMenuItem.submenu = editMenu
        mainMenu.addItem(editMenuItem)

        // Window menu
        let windowMenu = NSMenu(title: "Window")
        windowMenu.addItem(NSMenuItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m"))
        windowMenu.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
        windowMenu.addItem(.separator())
        windowMenu.addItem(NSMenuItem(title: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: ""))

        let windowMenuItem = NSMenuItem()
        windowMenuItem.submenu = windowMenu
        mainMenu.addItem(windowMenuItem)

        NSApp.mainMenu = mainMenu
        NSApp.windowsMenu = windowMenu
    }

    // MARK: - Actions

    @objc func newDocument(_ sender: Any?) {
        // TODO: Create new document
    }

    @objc func openDocument(_ sender: Any?) {
        // TODO: Open document
    }
}

Directory Structure:

Apps/
└── AppKitApp/
    ├── main.swift           # NSApplicationMain setup
    ├── AppDelegate.swift    # Import CanvasFeature, create window
    ├── MainMenu.swift       # Separate file for menu code (extension)
    ├── Info.plist
    └── Assets.xcassets/

Rules:

  1. AppKit uses main.swift + AppDelegate.swift (no @main on AppDelegate).
  2. Menu code lives in a separate file (MainMenu.swift).
  3. Menu code is an extension of AppDelegate.
  4. The feature target provides createWindowController() factory function.
  5. The feature target name MUST end with Feature (e.g., CanvasFeature, NOT Canvas)

Feature Target Exports

The feature target (e.g., AppFeature, CanvasFeature) MUST export:

For SwiftUI

// Packages/Sources/AppFeature/AppView.swift
import SwiftUI

public struct AppView: View {
    public init() {}

    public var body: some View {
        // App content
    }
}

For UIKit

// Packages/Sources/CanvasFeature/WindowFactory.swift
import UIKit

public enum CanvasFeature {
    @MainActor
    public static func createMainWindow(for windowScene: UIWindowScene) -> UIWindow {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = MainViewController()
        window.makeKeyAndVisible()
        return window
    }
}

For AppKit

// Packages/Sources/CanvasFeature/WindowFactory.swift
import AppKit

public enum CanvasFeature {
    @MainActor
    public static func createWindowController() -> NSWindowController {
        let viewController = MainViewController()
        let window = NSWindow(contentViewController: viewController)
        window.title = "MyApp"
        window.setContentSize(NSSize(width: 1200, height: 800))
        window.center()
        return NSWindowController(window: window)
    }
}

App Target Checklist

  • SwiftUI: App file has ONLY the feature-module import plus FeatureView()
  • SwiftUI: Both iOS and macOS use the SAME feature target
  • UIKit: SceneDelegate imports the feature module and uses its factory
  • AppKit: AppDelegate imports the feature module and uses its factory
  • AppKit: Menu code is in SEPARATE MainMenu.swift file
  • AppKit: main.swift exists with NSApplicationMain
  • Feature target name ends with Feature (NOT raw name like Canvas)
  • Feature target exports appropriate factory functions
  • Font registration (if needed) happens in app init BEFORE UI