mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
Add ios-spm-app-packaging skill with Tuist scaffold and build
This commit is contained in:
parent
a66c570852
commit
98b11b8ba6
11 changed files with 508 additions and 0 deletions
58
ios-spm-app-packaging/SKILL.md
Normal file
58
ios-spm-app-packaging/SKILL.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
name: ios-spm-app-packaging
|
||||
description: Scaffold, build, and package SwiftPM-based iOS apps using Tuist. Use when you need a from-scratch iOS app layout, SwiftPM dependencies, automated project generation, or signing/archiving/distribution steps outside manual Xcode project management.
|
||||
---
|
||||
|
||||
# iOS SwiftPM App Packaging (Tuist)
|
||||
|
||||
## Overview
|
||||
|
||||
Bootstrap a SwiftPM-based iOS app using Tuist to generate the Xcode project. Build, archive, and distribute without manually managing xcodeproj files. Tuist uses Swift-based manifests for type-safe configuration with IDE autocomplete.
|
||||
|
||||
## Two-Step Workflow
|
||||
|
||||
1) Bootstrap the project folder
|
||||
- Copy `assets/templates/bootstrap/` into a new repo.
|
||||
- Rename `MyApp` in `Package.swift`, `Sources/MyApp/`, and `Project.swift`.
|
||||
- Customize `APP_NAME`, `BUNDLE_ID`, and versions in `Project.swift`.
|
||||
|
||||
2) Build, archive, and distribute
|
||||
- Install Tuist: `brew install --cask tuist`.
|
||||
- Generate project: `tuist generate`.
|
||||
- Build: `tuist xcodebuild build -scheme MyApp -sdk iphonesimulator`.
|
||||
- Archive: `Scripts/archive.sh`.
|
||||
- Export IPA: `Scripts/export_ipa.sh`.
|
||||
- Upload to TestFlight: `Scripts/upload_testflight.sh`.
|
||||
|
||||
## Templates
|
||||
|
||||
- `assets/templates/bootstrap/`: Minimal SwiftPM iOS app skeleton with CI configuration, Gemfile, and SwiftLint config.
|
||||
- `assets/templates/archive.sh`: Create a release archive (supports `SKIP_SIGNING=1`).
|
||||
- `assets/templates/export_ipa.sh`: Export IPA from archive for distribution.
|
||||
- `assets/templates/upload_testflight.sh`: Upload IPA to App Store Connect.
|
||||
- `assets/templates/release.sh`: Complete release workflow (version bump, build, GitHub release, TestFlight).
|
||||
- `assets/templates/ExportOptions.plist`: Export options for app-store or ad-hoc distribution.
|
||||
- `assets/templates/fastlane/`: Fastfile and Appfile for optional Fastlane automation.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tuist regenerates the xcodeproj from `Project.swift`; never edit xcodeproj manually.
|
||||
- Add `*.xcodeproj` to `.gitignore` since it's generated.
|
||||
- Provisioning requires Apple Developer account and certificates configured in Keychain.
|
||||
- For CI without signing, use `SKIP_SIGNING=1 Scripts/archive.sh`.
|
||||
- Optional: Use `tuist cache warm` for faster builds via caching.
|
||||
|
||||
## Signing
|
||||
|
||||
Tuist defines build settings but doesn't manage certificates. Options:
|
||||
- **Manual**: Download certificates/profiles from Apple Developer portal.
|
||||
- **Automatic**: Let Xcode manage signing (local dev only).
|
||||
- **Fastlane match**: Automated certificate management via git repo (recommended for teams/CI).
|
||||
|
||||
## Reference material
|
||||
|
||||
- See `references/scaffold.md` for project setup details.
|
||||
- See `references/packaging.md` for build and archive configuration.
|
||||
- See `references/release.md` for TestFlight and App Store distribution.
|
||||
- See `references/fastlane.md` for optional Fastlane automation and `match` signing.
|
||||
- See `references/swiftlint.md` for code style enforcement and linting integration.
|
||||
10
ios-spm-app-packaging/assets/templates/ExportOptions.plist
Normal file
10
ios-spm-app-packaging/assets/templates/ExportOptions.plist
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?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>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
</dict>
|
||||
</plist>
|
||||
43
ios-spm-app-packaging/assets/templates/archive.sh
Normal file
43
ios-spm-app-packaging/assets/templates/archive.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Archive iOS app for distribution
|
||||
# Usage: ./archive.sh
|
||||
#
|
||||
# Environment variables:
|
||||
# SKIP_SIGNING=1 - Use CI configuration (no code signing)
|
||||
# APP_NAME - App name (default: MyApp)
|
||||
# SCHEME - Xcode scheme (default: APP_NAME or APP_NAME-CI if SKIP_SIGNING)
|
||||
# CONFIGURATION - Build configuration (default: Release or CI if SKIP_SIGNING)
|
||||
# BUILD_DIR - Output directory (default: build)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
APP_NAME="${APP_NAME:-MyApp}"
|
||||
BUILD_DIR="${BUILD_DIR:-build}"
|
||||
|
||||
# Use CI config/scheme when SKIP_SIGNING is set
|
||||
if [ "${SKIP_SIGNING:-0}" = "1" ]; then
|
||||
CONFIGURATION="${CONFIGURATION:-CI}"
|
||||
SCHEME="${SCHEME:-$APP_NAME-CI}"
|
||||
echo "Running in CI mode (no code signing)..."
|
||||
else
|
||||
CONFIGURATION="${CONFIGURATION:-Release}"
|
||||
SCHEME="${SCHEME:-$APP_NAME}"
|
||||
fi
|
||||
|
||||
echo "Regenerating Xcode project..."
|
||||
tuist generate
|
||||
|
||||
echo "Archiving $APP_NAME (scheme: $SCHEME, config: $CONFIGURATION)..."
|
||||
tuist xcodebuild archive \
|
||||
-scheme "$SCHEME" \
|
||||
-sdk iphoneos \
|
||||
-configuration "$CONFIGURATION" \
|
||||
-archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \
|
||||
-allowProvisioningUpdates
|
||||
|
||||
echo "Archive created: $BUILD_DIR/$APP_NAME.xcarchive"
|
||||
25
ios-spm-app-packaging/assets/templates/bootstrap/.gitignore
vendored
Normal file
25
ios-spm-app-packaging/assets/templates/bootstrap/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Xcode (generated by Tuist)
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
*.xcscmblueprint
|
||||
|
||||
# Tuist
|
||||
.tuist-derived/
|
||||
Derived/
|
||||
|
||||
# Build
|
||||
build/
|
||||
.build/
|
||||
|
||||
# Swift Package Manager
|
||||
.swiftpm/
|
||||
Package.resolved
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Archives
|
||||
*.xcarchive
|
||||
*.ipa
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApp",
|
||||
platforms: [.iOS(.v17)],
|
||||
products: [
|
||||
.library(name: "MyApp", targets: ["MyApp"])
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
path: "Sources/MyApp",
|
||||
resources: [.process("Resources")]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "MyApp",
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
destinations: .iOS,
|
||||
product: .app,
|
||||
bundleId: "com.example.myapp",
|
||||
deploymentTargets: .iOS("17.0"),
|
||||
infoPlist: .extendingDefault(with: [
|
||||
"UILaunchScreen": [:],
|
||||
"UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait"]
|
||||
]),
|
||||
sources: ["Sources/MyApp/**"],
|
||||
resources: ["Sources/MyApp/Resources/**"],
|
||||
settings: .settings(
|
||||
base: [
|
||||
"MARKETING_VERSION": "1.0.0",
|
||||
"CURRENT_PROJECT_VERSION": "1",
|
||||
"SWIFT_VERSION": "6.0"
|
||||
],
|
||||
configurations: [
|
||||
.debug(name: "Debug", settings: [
|
||||
"CODE_SIGN_IDENTITY": "Apple Development"
|
||||
]),
|
||||
.release(name: "Release", settings: [
|
||||
"CODE_SIGN_IDENTITY": "Apple Distribution"
|
||||
]),
|
||||
.release(name: "CI", settings: [
|
||||
"CODE_SIGNING_REQUIRED": "NO",
|
||||
"CODE_SIGNING_ALLOWED": "NO",
|
||||
"CODE_SIGN_IDENTITY": ""
|
||||
])
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
schemes: [
|
||||
.scheme(
|
||||
name: "MyApp",
|
||||
buildAction: .buildAction(targets: ["MyApp"]),
|
||||
runAction: .runAction(configuration: "Debug"),
|
||||
archiveAction: .archiveAction(configuration: "Release")
|
||||
),
|
||||
.scheme(
|
||||
name: "MyApp-CI",
|
||||
buildAction: .buildAction(targets: ["MyApp"]),
|
||||
runAction: .runAction(configuration: "Debug"),
|
||||
archiveAction: .archiveAction(configuration: "CI")
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MyApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
39
ios-spm-app-packaging/assets/templates/export_ipa.sh
Normal file
39
ios-spm-app-packaging/assets/templates/export_ipa.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Export IPA from archive
|
||||
# Usage: ./export_ipa.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
APP_NAME="${APP_NAME:-MyApp}"
|
||||
BUILD_DIR="${BUILD_DIR:-build}"
|
||||
EXPORT_OPTIONS="${EXPORT_OPTIONS:-ExportOptions.plist}"
|
||||
|
||||
if [ ! -f "$EXPORT_OPTIONS" ]; then
|
||||
echo "Creating default ExportOptions.plist for App Store..."
|
||||
cat > "$EXPORT_OPTIONS" << 'EOF'
|
||||
<?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>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Exporting IPA..."
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$BUILD_DIR/$APP_NAME.xcarchive" \
|
||||
-exportPath "$BUILD_DIR/export" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS" \
|
||||
-allowProvisioningUpdates
|
||||
|
||||
echo "IPA exported to: $BUILD_DIR/export/"
|
||||
110
ios-spm-app-packaging/references/packaging.md
Normal file
110
ios-spm-app-packaging/references/packaging.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Packaging notes
|
||||
|
||||
## Build commands
|
||||
|
||||
Generate project with Tuist:
|
||||
```bash
|
||||
tuist generate
|
||||
```
|
||||
|
||||
Build with tuist xcodebuild wrapper:
|
||||
```bash
|
||||
# Simulator build
|
||||
tuist xcodebuild build -scheme MyApp -sdk iphonesimulator -configuration Debug
|
||||
|
||||
# Device build (requires signing)
|
||||
tuist xcodebuild build -scheme MyApp -sdk iphoneos -configuration Release
|
||||
```
|
||||
|
||||
## Archive for distribution
|
||||
|
||||
```bash
|
||||
tuist generate
|
||||
tuist xcodebuild archive \
|
||||
-scheme MyApp \
|
||||
-sdk iphoneos \
|
||||
-configuration Release \
|
||||
-archivePath build/MyApp.xcarchive
|
||||
```
|
||||
|
||||
## Export IPA
|
||||
|
||||
Create `ExportOptions.plist`:
|
||||
```xml
|
||||
<?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>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
Export:
|
||||
```bash
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath build/MyApp.xcarchive \
|
||||
-exportPath build/export \
|
||||
-exportOptionsPlist ExportOptions.plist
|
||||
```
|
||||
|
||||
## Common environment variables
|
||||
|
||||
- `APP_NAME`: App/target name.
|
||||
- `BUNDLE_ID`: Bundle identifier.
|
||||
- `TEAM_ID`: Apple Developer Team ID.
|
||||
- `SCHEME`: Xcode scheme name.
|
||||
- `CONFIGURATION`: `Debug`, `Release`, or `CI`.
|
||||
- `SKIP_SIGNING`: Set to `1` to use CI configuration (no code signing).
|
||||
|
||||
## Signing modes
|
||||
|
||||
| Method | Use Case |
|
||||
|--------|----------|
|
||||
| Automatic | Local dev with Xcode managing profiles |
|
||||
| Manual | CI with exported provisioning profiles |
|
||||
| Ad-hoc | Internal testing outside TestFlight |
|
||||
| App Store | TestFlight and App Store distribution |
|
||||
|
||||
## CI / no-signing environments
|
||||
|
||||
Archive commands fail without valid signing certificates. The project includes a `CI` configuration and `MyApp-CI` scheme for testing the archive pipeline without signing.
|
||||
|
||||
**Why archive fails without certs:**
|
||||
- `xcodebuild archive` requires code signing even for test builds
|
||||
- CI environments often lack provisioning profiles
|
||||
|
||||
**Use the CI configuration:**
|
||||
```bash
|
||||
# Archive without signing (uses CI config)
|
||||
SKIP_SIGNING=1 Scripts/archive.sh
|
||||
|
||||
# Or manually:
|
||||
tuist xcodebuild archive \
|
||||
-scheme MyApp-CI \
|
||||
-sdk iphoneos \
|
||||
-configuration CI \
|
||||
-archivePath build/MyApp.xcarchive
|
||||
```
|
||||
|
||||
**For real distribution** (requires Apple Developer account):
|
||||
```bash
|
||||
tuist generate
|
||||
tuist xcodebuild archive \
|
||||
-scheme MyApp \
|
||||
-sdk iphoneos \
|
||||
-configuration Release \
|
||||
-allowProvisioningUpdates
|
||||
```
|
||||
|
||||
## Tuist caching (optional)
|
||||
|
||||
Speed up builds by caching dependencies:
|
||||
```bash
|
||||
tuist cache warm
|
||||
tuist generate
|
||||
tuist xcodebuild build
|
||||
```
|
||||
130
ios-spm-app-packaging/references/scaffold.md
Normal file
130
ios-spm-app-packaging/references/scaffold.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Scaffold a SwiftPM iOS app (Tuist)
|
||||
|
||||
## Steps
|
||||
|
||||
1) Create a repo and initialize SwiftPM:
|
||||
```
|
||||
mkdir MyApp
|
||||
cd MyApp
|
||||
swift package init --type library
|
||||
```
|
||||
|
||||
2) Create `Project.swift` for Tuist (see template below).
|
||||
|
||||
3) Create the app entry point under `Sources/MyApp/`.
|
||||
- Use SwiftUI with `@main` App struct.
|
||||
|
||||
4) Add resources directory:
|
||||
```
|
||||
mkdir -p Sources/MyApp/Resources
|
||||
```
|
||||
|
||||
5) Install Tuist and generate project:
|
||||
```
|
||||
brew install --cask tuist
|
||||
tuist generate
|
||||
open MyApp.xcodeproj
|
||||
```
|
||||
|
||||
6) Add generated files to `.gitignore`:
|
||||
```
|
||||
echo "*.xcodeproj" >> .gitignore
|
||||
echo ".tuist-derived/" >> .gitignore
|
||||
```
|
||||
|
||||
## Minimal Package.swift
|
||||
|
||||
```swift
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApp",
|
||||
platforms: [.iOS(.v17)],
|
||||
products: [
|
||||
.library(name: "MyApp", targets: ["MyApp"])
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
path: "Sources/MyApp",
|
||||
resources: [.process("Resources")]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Minimal Project.swift (Tuist)
|
||||
|
||||
```swift
|
||||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "MyApp",
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
destinations: .iOS,
|
||||
product: .app,
|
||||
bundleId: "com.example.myapp",
|
||||
deploymentTargets: .iOS("17.0"),
|
||||
infoPlist: .extendingDefault(with: [
|
||||
"UILaunchScreen": [:]
|
||||
]),
|
||||
sources: ["Sources/MyApp/**"],
|
||||
resources: ["Sources/MyApp/Resources/**"],
|
||||
settings: .settings(base: [
|
||||
"MARKETING_VERSION": "1.0.0",
|
||||
"CURRENT_PROJECT_VERSION": "1",
|
||||
"SWIFT_VERSION": "6.0"
|
||||
])
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Minimal SwiftUI entry point
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MyApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding SPM dependencies
|
||||
|
||||
In `Project.swift`, add packages and dependencies:
|
||||
|
||||
```swift
|
||||
let project = Project(
|
||||
name: "MyApp",
|
||||
packages: [
|
||||
.remote(url: "https://github.com/Alamofire/Alamofire", requirement: .upToNextMajor(from: "5.0.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
destinations: .iOS,
|
||||
product: .app,
|
||||
bundleId: "com.example.myapp",
|
||||
sources: ["Sources/MyApp/**"],
|
||||
dependencies: [
|
||||
.package(product: "Alamofire")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
Loading…
Reference in a new issue