10 KiB
10 KiB
Project Scaffolding
Complete setup guide for new iOS projects with CLI-only development workflow.
XcodeGen Setup (Recommended)
Install XcodeGen (one-time):
brew install xcodegen
Create a new iOS app:
mkdir MyApp && cd MyApp
mkdir -p MyApp/{App,Models,Views,Services,Resources} MyAppTests MyAppUITests
# Create project.yml (see template below)
# Create Swift files
xcodegen generate
xcodebuild -project MyApp.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16' build
project.yml Template
Complete iOS SwiftUI app with tests:
name: MyApp
options:
bundleIdPrefix: com.yourcompany
deploymentTarget:
iOS: "18.0"
xcodeVersion: "16.0"
createIntermediateGroups: true
configs:
Debug: debug
Release: release
settings:
base:
SWIFT_VERSION: "5.9"
IPHONEOS_DEPLOYMENT_TARGET: "18.0"
TARGETED_DEVICE_FAMILY: "1,2"
targets:
MyApp:
type: application
platform: iOS
sources:
- MyApp
resources:
- path: MyApp/Resources
excludes:
- "**/.DS_Store"
info:
path: MyApp/Info.plist
properties:
UILaunchScreen: {}
CFBundleName: $(PRODUCT_NAME)
CFBundleIdentifier: $(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
UIRequiredDeviceCapabilities:
- armv7
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
entitlements:
path: MyApp/MyApp.entitlements
properties:
aps-environment: development
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp
PRODUCT_NAME: MyApp
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: YOURTEAMID
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
configs:
Debug:
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
SWIFT_OPTIMIZATION_LEVEL: -Onone
Release:
SWIFT_OPTIMIZATION_LEVEL: -Osize
MyAppTests:
type: bundle.unit-test
platform: iOS
sources:
- MyAppTests
dependencies:
- target: MyApp
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp.tests
MyAppUITests:
type: bundle.ui-testing
platform: iOS
sources:
- MyAppUITests
dependencies:
- target: MyApp
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.myapp.uitests
TEST_TARGET_NAME: MyApp
schemes:
MyApp:
build:
targets:
MyApp: all
MyAppTests: [test]
MyAppUITests: [test]
run:
config: Debug
test:
config: Debug
gatherCoverageData: true
targets:
- MyAppTests
- MyAppUITests
profile:
config: Release
archive:
config: Release
project.yml with SwiftData
Add SwiftData support:
targets:
MyApp:
# ... existing config ...
settings:
base:
# ... existing settings ...
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) SWIFT_DATA"
dependencies:
- sdk: SwiftData.framework
project.yml with Swift Packages
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
For users who prefer Xcode:
- File > New > Project > iOS > App
- Settings: SwiftUI, Swift, SwiftData (optional)
- Save and close Xcode
File Structure
MyApp/
├── MyApp.xcodeproj/
├── MyApp/
│ ├── App/
│ │ ├── MyApp.swift
│ │ ├── AppState.swift
│ │ └── AppDependencies.swift
│ ├── Models/
│ ├── Views/
│ │ ├── ContentView.swift
│ │ ├── Screens/
│ │ └── Components/
│ ├── Services/
│ ├── Utilities/
│ ├── Resources/
│ │ ├── Assets.xcassets/
│ │ ├── Localizable.xcstrings
│ │ └── PrivacyInfo.xcprivacy
│ ├── Info.plist
│ └── MyApp.entitlements
├── MyAppTests/
└── MyAppUITests/
Starter Code
MyApp.swift
import SwiftUI
@main
struct MyApp: App {
@State private var appState = AppState()
init() {
configureAppearance()
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState)
.task {
await appState.initialize()
}
}
}
private func configureAppearance() {
// Global appearance customization
}
}
AppState.swift
import SwiftUI
@Observable
class AppState {
// Navigation
var navigationPath = NavigationPath()
var selectedTab: Tab = .home
// App state
var isLoading = false
var error: AppError?
var user: User?
// Feature flags
var isPremium = false
enum Tab: Hashable {
case home, search, profile
}
func initialize() async {
// Load initial data
// Check purchase status
// Request permissions if needed
}
func handleDeepLink(_ url: URL) {
// Parse URL and update navigation
}
}
enum AppError: LocalizedError {
case networkError(Error)
case dataError(String)
case unauthorized
var errorDescription: String? {
switch self {
case .networkError(let error):
return error.localizedDescription
case .dataError(let message):
return message
case .unauthorized:
return "Please sign in to continue"
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
@Environment(AppState.self) private var appState
var body: some View {
@Bindable var appState = appState
TabView(selection: $appState.selectedTab) {
HomeScreen()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(AppState.Tab.home)
SearchScreen()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(AppState.Tab.search)
ProfileScreen()
.tabItem {
Label("Profile", systemImage: "person")
}
.tag(AppState.Tab.profile)
}
.overlay {
if appState.isLoading {
LoadingOverlay()
}
}
.alert("Error", isPresented: .constant(appState.error != nil)) {
Button("OK") { appState.error = nil }
} message: {
if let error = appState.error {
Text(error.localizedDescription)
}
}
}
}
Privacy Manifest
Required for App Store submission. Create PrivacyInfo.xcprivacy:
<?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>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<!-- Add collected data types here -->
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
Entitlements 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>
<!-- Push Notifications -->
<key>aps-environment</key>
<string>development</string>
<!-- App Groups (for shared data) -->
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourcompany.myapp</string>
</array>
</dict>
</plist>
Xcode Project Creation
Create via command line using xcodegen or tuist, or create in Xcode and immediately close:
# Option 1: Using xcodegen
brew install xcodegen
# Create project.yml, then:
xcodegen generate
# Option 2: Create in Xcode, configure, close
# File > New > Project > iOS > App
# Configure settings, then close Xcode
Build Configuration
Development vs Release
# Debug build
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build
# Release build
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
build
Environment Variables
Use xcconfig files for different environments:
// Debug.xcconfig
API_BASE_URL = https://dev-api.example.com
ENABLE_LOGGING = YES
// Release.xcconfig
API_BASE_URL = https://api.example.com
ENABLE_LOGGING = NO
Access in code:
let apiURL = Bundle.main.infoDictionary?["API_BASE_URL"] as? String
Asset Catalog Setup
App Icon
- Provide 1024x1024 PNG
- Xcode generates all sizes automatically
Colors
Define semantic colors in Assets.xcassets:
AccentColor- App tint colorBackgroundPrimary- Main backgroundTextPrimary- Primary text
SF Symbols
Prefer SF Symbols for icons. Use custom symbols only when necessary.
Localization Setup
- Enable localization in project settings
- Create
Localizable.xcstrings(Xcode 15+) - Use String Catalogs for automatic extraction
// Strings are automatically extracted
Text("Welcome")
Text("Items: \(count)")