Advanced CI/CD for Swift Projects: Optimizing Pipelines with GitHub Actions
Automation of building, testing, and deployment is an integral part of modern iOS and macOS development. Basic CI/CD pipelines solve simple tasks, but real-world projects with Swift, Swift Package Manager (SPM), multiple targets, and complex dependencies require an advanced approach. In this article, we will explore how to take your CI/CD process to the next level using GitHub Actions, leveraging matrix builds, caching optimization, parallel testing, and automatic deployment to TestFlight / App Store.
1. Matrix Builds for Multi-Platform Support
One of the key features of GitHub Actions is the matrix strategy. It allows you to run the same workflow with different parameters (Xcode version, build scheme, platform). This is especially useful for Swift projects that support iOS, macOS, tvOS, and watchOS.
Example matrix configuration for a Swift project:
jobs: build: strategy: matrix: platform: [iOS, macOS, tvOS] xcode-version: ['15.0', '15.2'] exclude: - platform: tvOS xcode-version: '15.0' # Exclude incompatible combination runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode-version }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode-version }}.app - name: Build for ${{ matrix.platform }} run: | xcodebuild clean build \ -scheme MyApp \ -destination "platform=${{ matrix.platform }}" \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NOThis approach allows you to verify the build on all supported platforms and Xcode versions with a single push, saving developer time.
2. Advanced Caching: Speed Up Builds by 2-3 Times
Building Swift projects is a resource-intensive process. Caching SPM dependencies and DerivedData can reduce pipeline time by 50-70%. Instead of a basic cache, use custom keys that account for dependency versions.
Optimized cache for SPM and Xcode:
- name: Cache SPM uses: actions/cache@v3 with: path: | .build SourcePackages key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} restore-keys: | ${{ runner.os }}-spm-
- name: Cache DerivedData uses: actions/cache@v3 with: path: ~/Library/Developer/Xcode/DerivedData key: ${{ runner.os }}-derived-${{ hashFiles('**/*.swift') }} restore-keys: | ${{ runner.os }}-derived-Important: Use hashFiles('**/Package.resolved') for precise cache invalidation when dependencies change. For DerivedData, the key can be the hash of all Swift files — this ensures the cache is updated only when there are actual code changes.
3. Parallel Testing and Reporting
For large Swift projects with hundreds of unit tests, sequential execution can take 20-30 minutes. The solution is splitting tests into groups and running them in parallel on multiple runners.
Implementing parallel testing:
test: strategy: matrix: test-group: [1, 2, 3, 4] runs-on: macos-14 steps: - uses: actions/checkout@v4 - name: Run tests (Group ${{ matrix.test-group }}) run: | xcodebuild test \ -scheme MyApp \ -destination "platform=iOS Simulator,name=iPhone 15 Pro" \ -only-testing:MyAppTests/TestGroup${{ matrix.test-group }} \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: