Files
gh-glittercowboy-taches-cc-…/skills/expertise/iphone-apps/references/project-scaffolding.md
2025-11-29 18:28:37 +08:00

10 KiB

Project Scaffolding

Complete setup guide for new iOS projects with CLI-only development workflow.

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:

  1. File > New > Project > iOS > App
  2. Settings: SwiftUI, Swift, SwiftData (optional)
  3. 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 color
  • BackgroundPrimary - Main background
  • TextPrimary - Primary text

SF Symbols

Prefer SF Symbols for icons. Use custom symbols only when necessary.

Localization Setup

  1. Enable localization in project settings
  2. Create Localizable.xcstrings (Xcode 15+)
  3. Use String Catalogs for automatic extraction
// Strings are automatically extracted
Text("Welcome")
Text("Items: \(count)")