Project Scaffolding
Complete setup for new macOS Swift apps with all necessary files and configurations.
Overview
Project Scaffolding
Complete setup for new macOS Swift apps with all necessary files and configurations.
<new_project_checklist>
- Create project.yml for XcodeGen
- Create Swift source files
- Run
xcodegen generate - Configure signing (DEVELOPMENT_TEAM)
- Build and verify with
xcodebuild</new_project_checklist>
<xcodegen_setup> Install XcodeGen (one-time):
brew install xcodegen
Create a new macOS app:
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
</xcodegen_setup>
<project_yml_template> project.yml - Complete macOS SwiftUI app template:
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_template>
<project_yml_swiftdata> project.yml with SwiftData:
Add to target settings:
settings:
base:
# ... existing settings ...
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) SWIFT_DATA"
dependencies:
- sdk: SwiftData.framework
</project_yml_swiftdata>
<project_yml_packages> Adding Swift Package dependencies:
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
</project_yml_packages>
<alternative_xcode_template> Alternative: Xcode GUI method
For users who prefer Xcode:
- File > New > Project > macOS > App
- Settings: SwiftUI, Swift, SwiftData (optional)
- Save to desired location </alternative_xcode_template>
<minimal_file_structure>
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
</minimal_file_structure>
<starter_code> <app_entry_point> MyApp.swift:
@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()
}
}
}
</app_entry_point>
<app_state> AppState.swift:
@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()
}
</app_state>
<content_view> ContentView.swift:
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")
}
}
}
</content_view>
<settings_view> SettingsView.swift:
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()
}
}
</settings_view> </starter_code>
<info_plist> Info.plist (complete template):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleDisplayName</key>
<string>MyApp</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2024 Your Name. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
</dict>
</plist>
Common category types:
public.app-category.productivitypublic.app-category.developer-toolspublic.app-category.utilitiespublic.app-category.musicpublic.app-category.graphics-design</info_plist>
Debug entitlements (add for debug builds):
<key>com.apple.security.get-task-allow</key>
<true/>
</entitlements>
<assets_catalog> Assets.xcassets/Contents.json:
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Assets.xcassets/AppIcon.appiconset/Contents.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:
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
</assets_catalog>
<swift_packages> Add dependencies via Package.swift or Xcode:
Common packages:
// 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:
# Edit project to add package dependency
# (Easier to do once in Xcode, then clone for future projects)
</swift_packages>
<verify_setup>
# 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
</verify_setup>
<next_steps> After scaffolding:
- Define your data model: Create models in Models/ folder
- Choose persistence: SwiftData, Core Data, or file-based
- Design main UI: Sidebar + detail or single-window layout
- Add menu commands: Edit AppCommands.swift
- Configure logging: Set up os.Logger with appropriate subsystem
- Write tests: Unit tests for models, integration tests for services
See cli-workflow.md for build/run/debug workflow. </next_steps>