13 KiB
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:
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()
}
}
}
</app_entry_point>
<app_state> AppState.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()
}
</app_state>
<content_view> ContentView.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")
}
}
}
</content_view>
<settings_view> SettingsView.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()
}
}
</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/>
<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>