# Project Scaffolding
Complete setup for new macOS Swift apps with all necessary files and configurations.
1. Create project.yml for XcodeGen
2. Create Swift source files
3. Run `xcodegen generate`
4. Configure signing (DEVELOPMENT_TEAM)
5. Build and verify with `xcodebuild`
**Install XcodeGen** (one-time):
```bash
brew install xcodegen
```
**Create a new macOS app**:
```bash
mkdir MyApp && cd MyApp
mkdir -p Sources Tests Resources
# Create project.yml (see template below)
# Create Swift files
xcodegen generate
xcodebuild -project MyApp.xcodeproj -scheme MyApp build
```
**project.yml** - Complete macOS SwiftUI app template:
```yaml
name: MyApp
options:
bundleIdPrefix: com.yourcompany
deploymentTarget:
macOS: "14.0"
xcodeVersion: "15.0"
createIntermediateGroups: true
configs:
Debug: debug
Release: release
settings:
base:
SWIFT_VERSION: "5.9"
MACOSX_DEPLOYMENT_TARGET: "14.0"
targets:
MyApp:
type: application
platform: macOS
sources:
- Sources
resources:
- Resources
info:
path: Sources/Info.plist
properties:
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
CFBundleName: $(PRODUCT_NAME)
CFBundleIdentifier: $(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
LSApplicationCategoryType: public.app-category.utilities
NSPrincipalClass: NSApplication
NSHighResolutionCapable: true
entitlements:
path: Sources/MyApp.entitlements
properties:
com.apple.security.app-sandbox: true
com.apple.security.network.client: true
com.apple.security.files.user-selected.read-write: true
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp
PRODUCT_NAME: MyApp
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: YOURTEAMID
configs:
Debug:
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
SWIFT_OPTIMIZATION_LEVEL: -Onone
CODE_SIGN_ENTITLEMENTS: Sources/MyApp.entitlements
Release:
SWIFT_OPTIMIZATION_LEVEL: -Osize
MyAppTests:
type: bundle.unit-test
platform: macOS
sources:
- Tests
dependencies:
- target: MyApp
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp.tests
schemes:
MyApp:
build:
targets:
MyApp: all
MyAppTests: [test]
run:
config: Debug
test:
config: Debug
gatherCoverageData: true
targets:
- MyAppTests
profile:
config: Release
archive:
config: Release
```
**project.yml with SwiftData**:
Add to target settings:
```yaml
settings:
base:
# ... existing settings ...
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) SWIFT_DATA"
dependencies:
- sdk: SwiftData.framework
```
**Adding Swift Package dependencies**:
```yaml
packages:
Alamofire:
url: https://github.com/Alamofire/Alamofire
from: 5.8.0
KeychainAccess:
url: https://github.com/kishikawakatsumi/KeychainAccess
from: 4.2.0
targets:
MyApp:
# ... other config ...
dependencies:
- package: Alamofire
- package: KeychainAccess
```
**Alternative: Xcode GUI method**
For users who prefer Xcode:
1. File > New > Project > macOS > App
2. Settings: SwiftUI, Swift, SwiftData (optional)
3. Save to desired location
```
MyApp/
├── MyApp.xcodeproj/
│ └── project.pbxproj
├── MyApp/
│ ├── MyApp.swift # App entry point
│ ├── ContentView.swift # Main view
│ ├── Info.plist
│ ├── MyApp.entitlements
│ └── Assets.xcassets/
│ ├── Contents.json
│ ├── AppIcon.appiconset/
│ │ └── Contents.json
│ └── AccentColor.colorset/
│ └── Contents.json
└── MyAppTests/
└── MyAppTests.swift
```
**MyApp.swift**:
```swift
import SwiftUI
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState)
}
.commands {
CommandGroup(replacing: .newItem) { } // Remove default New
}
Settings {
SettingsView()
}
}
}
```
**AppState.swift**:
```swift
import SwiftUI
@Observable
class AppState {
var items: [Item] = []
var selectedItemID: UUID?
var searchText = ""
var selectedItem: Item? {
items.first { $0.id == selectedItemID }
}
var filteredItems: [Item] {
if searchText.isEmpty {
return items
}
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
func addItem(_ name: String) {
let item = Item(name: name)
items.append(item)
selectedItemID = item.id
}
func deleteItem(_ item: Item) {
items.removeAll { $0.id == item.id }
if selectedItemID == item.id {
selectedItemID = nil
}
}
}
struct Item: Identifiable, Hashable {
let id = UUID()
var name: String
var createdAt = Date()
}
```
**ContentView.swift**:
```swift
import SwiftUI
struct ContentView: View {
@Environment(AppState.self) private var appState
var body: some View {
@Bindable var appState = appState
NavigationSplitView {
SidebarView()
} detail: {
DetailView()
}
.searchable(text: $appState.searchText)
.navigationTitle("MyApp")
}
}
struct SidebarView: View {
@Environment(AppState.self) private var appState
var body: some View {
@Bindable var appState = appState
List(appState.filteredItems, selection: $appState.selectedItemID) { item in
Text(item.name)
.tag(item.id)
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add", systemImage: "plus")
}
}
}
}
private func addItem() {
appState.addItem("New Item")
}
}
struct DetailView: View {
@Environment(AppState.self) private var appState
var body: some View {
if let item = appState.selectedItem {
VStack {
Text(item.name)
.font(.title)
Text(item.createdAt.formatted())
.foregroundStyle(.secondary)
}
.padding()
} else {
ContentUnavailableView("No Selection", systemImage: "sidebar.left")
}
}
}
```
**SettingsView.swift**:
```swift
import SwiftUI
struct SettingsView: View {
var body: some View {
TabView {
GeneralSettingsView()
.tabItem {
Label("General", systemImage: "gear")
}
AdvancedSettingsView()
.tabItem {
Label("Advanced", systemImage: "slider.horizontal.3")
}
}
.frame(width: 450, height: 250)
}
}
struct GeneralSettingsView: View {
@AppStorage("showWelcome") private var showWelcome = true
@AppStorage("defaultName") private var defaultName = "Untitled"
var body: some View {
Form {
Toggle("Show welcome screen on launch", isOn: $showWelcome)
TextField("Default item name", text: $defaultName)
}
.padding()
}
}
struct AdvancedSettingsView: View {
@AppStorage("enableLogging") private var enableLogging = false
var body: some View {
Form {
Toggle("Enable debug logging", isOn: $enableLogging)
}
.padding()
}
}
```
**Info.plist** (complete template):
```xml
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleName
$(PRODUCT_NAME)
CFBundleDisplayName
MyApp
CFBundleVersion
1
CFBundleShortVersionString
1.0
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
Copyright © 2024 Your Name. All rights reserved.
NSPrincipalClass
NSApplication
NSHighResolutionCapable
LSApplicationCategoryType
public.app-category.productivity
```
**Common category types**:
- `public.app-category.productivity`
- `public.app-category.developer-tools`
- `public.app-category.utilities`
- `public.app-category.music`
- `public.app-category.graphics-design`
**MyApp.entitlements** (sandbox with network):
```xml
com.apple.security.app-sandbox
com.apple.security.network.client
com.apple.security.files.user-selected.read-write
```
**Debug entitlements** (add for debug builds):
```xml
com.apple.security.get-task-allow
```
**Assets.xcassets/Contents.json**:
```json
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
**Assets.xcassets/AppIcon.appiconset/Contents.json**:
```json
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
**Assets.xcassets/AccentColor.colorset/Contents.json**:
```json
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
Add dependencies via Package.swift or Xcode:
**Common packages**:
```swift
// In Xcode: File > Add Package Dependencies
// Networking
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
// Logging
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0")
// Keychain
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.0")
// Syntax highlighting
.package(url: "https://github.com/raspu/Highlightr.git", from: "2.1.0")
```
**Add via CLI**:
```bash
# Edit project to add package dependency
# (Easier to do once in Xcode, then clone for future projects)
```
```bash
# Verify project configuration
xcodebuild -list -project MyApp.xcodeproj
# Build
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug \
-derivedDataPath ./build \
build
# Run
open ./build/Build/Products/Debug/MyApp.app
# Check signing
codesign -dv ./build/Build/Products/Debug/MyApp.app
```
After scaffolding:
1. **Define your data model**: Create models in Models/ folder
2. **Choose persistence**: SwiftData, Core Data, or file-based
3. **Design main UI**: Sidebar + detail or single-window layout
4. **Add menu commands**: Edit AppCommands.swift
5. **Configure logging**: Set up os.Logger with appropriate subsystem
6. **Write tests**: Unit tests for models, integration tests for services
See [cli-workflow.md](cli-workflow.md) for build/run/debug workflow.