380 lines
11 KiB
Markdown
380 lines
11 KiB
Markdown
# 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)
|
|
|
|
```swift
|
|
// 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)
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```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
|
|
|
|
```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
|
|
|
|
```swift
|
|
// Apps/AppKitApp/main.swift
|
|
import Cocoa
|
|
|
|
MainActor.assumeIsolated {
|
|
let delegate = AppDelegate()
|
|
NSApplication.shared.delegate = delegate
|
|
}
|
|
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
|
```
|
|
|
|
#### AppDelegate.swift
|
|
|
|
```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)
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
// Packages/Sources/AppFeature/AppView.swift
|
|
import SwiftUI
|
|
|
|
public struct AppView: View {
|
|
public init() {}
|
|
|
|
public var body: some View {
|
|
// App content
|
|
}
|
|
}
|
|
```
|
|
|
|
#### For UIKit
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
// 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
|