Instructions
- Overview: This workflow outlines best practices to build, test,
and deploy a SwiftUI app targeting both iOS and macOS (with an
option for Catalyst). It covers project setup, architecture choices,
development practices, and continuous delivery steps.
- Prerequisites: Ensure you have the latest Xcode installed (Xcode
15 or newer) and are enrolled in the Apple Developer Program (required
for code signing, Xcode Cloud, and TestFlight). Familiarity with
SwiftUI, Git source control, and basic iOS/macOS app development is
assumed.
- Usage: Follow the steps below in sequence to configure a
multi-platform Xcode project, manage dependencies, implement an
architecture (MVVM or TCA), debug and profile efficiently, set up
CI/CD pipelines, and finally distribute the app via TestFlight.
- Conventions: This guide uses bold titles for key actions and
italics for tool names or concepts. Replace example placeholders
(like bundle identifiers or scheme names) with your own
project-specific values when applying these steps.
Workflow
Create a Multiplatform Xcode Project: Begin by creating a new
Xcode project that supports both iOS and macOS targets. Xcode 14+
offers a Multiplatform App template, which sets up a single
target capable of building for iOS (and iPadOS) and macOS using
SwiftUI. This unified target shares most code and assets across
platforms. If you prefer separate targets, you can instead create an
iOS app and then add a macOS target (or enable Mac Catalyst for the
iOS target). Ensure that shared code (like SwiftUI views and models)
is grouped in a cross-platform group, and use platform checks
(#if os(iOS), #if os(macOS)) for any platform-specific code. By
structuring the project as a shared codebase, you minimize
duplication while still tailoring the UI where necessary for each
device type.
Set Up Targets, Schemes, and Configurations: With a
multi-platform project, Xcode may already include separate
configurations for Debug and Release. You might add custom Build
Configurations (e.g. Staging or QA) if needed. If using separate
targets (for Catalyst or environment flavors), give each target a
unique Bundle Identifier and Info.plist. For example, suffix the
bundle ID with ".mac" for a macOS target or ".dev" for a
development build. Create corresponding Schemes for each app
target or environment so you can easily run and archive each
version. Mark schemes as "Shared" to include them in source control
(important for team use and CI). This multi-target setup allows you
to, for instance, have an iOS app, a native macOS app, and even a
Catalyst app all in one project, each with its own scheme and bundle
ID.
Configure Environment Settings: Manage environment-specific
settings by using Xcode's build configuration options. For example,
you can define custom XCConfig files or use User-Defined Build
Settings for values like API endpoints or feature flags. Define
keys in Info.plist that reference these settings. For instance, add
a key for BaseURL in your Info.plist and assign it a value like
$(BASE_URL) which is set per configuration. In Build Settings,
create a user-defined variable BASE_URL for each configuration
(e.g. Dev, QA, Prod), each pointing to the appropriate URL. Your app
can read these at runtime, for example:
let apiURL = Bundle.main.object(forInfoDictionaryKey: "BaseURL") as? String
This way, you avoid hardcoding environment values. Additionally,
consider using Compiler Flags for conditional code. In each
configuration's build settings, you might add Swift flags like
-DDEVELOPMENT or -DPRODUCTION. Then in Swift code, use
#if DEVELOPMENT to include debug-only logic or use placeholders for
testing. This approach keeps configuration differences isolated at build
time. Finally, ensure each app target uses distinct app icons and names
if needed (e.g. add suffix "Dev" to the app name for a development
build) -- you can set this via Info.plist or Asset catalogs per target.
Manage Dependencies (SPM and CocoaPods): Use Swift Package
Manager (SPM) as the primary tool for adding libraries and
frameworks. SPM is built into Xcode, making dependency management
seamless for SwiftUI projects. To add a package, go to File ▸ Add
Packages... and enter the package Git URL. Target the dependency
to your app target (and not to any CocoaPods-generated target) so
the package integrates correctly. SPM automatically fetches and
updates packages and keeps them sandboxed within Xcode. Commit the
Package.resolved file so that team members and CI use the same
versions. If you need a library that isn't available via SPM (or
contains significant Objective-C/legacy code), you can integrate
CocoaPods. Initialize a Podfile (pod init) and specify pods,
then run pod install to generate an .xcworkspace. Continue
working from the workspace thereafter. It's possible to mix SPM and
CocoaPods in one project -- just ensure that when adding SPM
packages you select your main project in the add dialog (not the
Pods project). Keep your Pod dependencies updated with pod update
as needed. In general, prefer SPM for pure Swift dependencies due to
its native Xcode support and ease of use, using CocoaPods only for
exceptions. Maintain clear documentation of third-party packages in
your README.
Apply an Architecture Pattern (MVVM or TCA): Structure your
SwiftUI code using a robust architecture to manage complexity. A
popular choice is MVVM (Model-View-ViewModel), which works
naturally with SwiftUI's data binding. In MVVM, define your data
models to represent app data, use SwiftUI Views for the UI, and
create ViewModel classes (conforming to ObservableObject) to
handle business logic and state. For example, a GameViewModel
might publish a @Published var score and handle methods to update
the score. The corresponding GameView uses @StateObject or
@ObservedObject to watch the ViewModel and update the UI. This
separation keeps the SwiftUI view declarative and lightweight, while
logic lives in the ViewModel (making it easier to test). For larger
apps or more complex state management, consider adopting The
Composable Architecture (TCA). TCA is a library (addable via SPM)
that follows a unidirectional data flow (inspired by Redux). You
break down your app into State, Actions, and Reducers. A
Reducer is a pure function that takes the current State and an
Action and produces a new State (and optionally, side effects known
as Effects). A Store connects your SwiftUI View to the state and
business logic: the View sends Actions (for example, button taps),
which the Store receives and feeds into the Reducer, updating State
which then flows back to the View. TCA encourages a very modular
structure: you can compose small features into larger ones, and it
provides tools to manage dependencies and side effects in a
controlled way. While TCA has a learning curve, it excels in
testability (you can easily write tests for reducer logic) and
scalability for big apps. Choose either MVVM (simpler, uses
SwiftUI's built-in reactive state features) or TCA (more structured,
ideal for complex apps) depending on project needs -- both will help
maintain a clear separation of concerns in your code.
Debugging and Profiling Practices: During development, use
Xcode's robust debugging tools to catch and fix issues early. Set
breakpoints in your code (by clicking the gutter next to a line
number) to pause execution and inspect variables at runtime. While
paused, use the LLDB console (po command) to print out values
or call functions to verify state. This is invaluable for logic in
ViewModels or TCA reducers where you want to ensure the correct data
flow. For SwiftUI views, the View Hierarchy Debugger is
extremely useful: run your app in Simulator and choose Debug ▸
View Debugging ▸ Capture View Hierarchy. This lets you inspect the
UI layout after a pause, so you can pinpoint why a view might not
appear or is misplaced. Additionally, leverage Instruments for
profiling. Xcode's Instruments app offers templates like Time
Profiler (to measure CPU performance and find slow functions) and
Leaks (to detect memory leaks and retain cycles). For example,
if a SwiftUI view is laggy, run Time Profiler while interacting with
it to identify expensive computations. SwiftUI in Xcode 15+ also
includes a dedicated "SwiftUI" animation and rendering timeline
instrument to analyze UI performance. Always test and profile in
Release mode periodically as well, since SwiftUI performance can
differ between Debug and Release builds. Use os_log or print
statements for lightweight debugging, especially to trace execution
paths or data changes (just be sure to remove or disable noisy logs
in production). By regularly debugging and profiling throughout
development, you ensure the app runs smoothly and catches bugs
before release.
Write Unit and UI Tests: Set up automated tests to maintain code
quality. Xcode can generate a Unit Test target and a UI Test target
when you create the project (you can also add them manually via
File ▸ New ▸ Target if not present). Unit Tests (using the
XCTest framework) should cover your core business logic. For MVVM,
test your ViewModel methods and state changes independently of the
UI. For TCA, you can leverage the TCA testing utilities to send
actions to your reducers and assert on state changes or effect
outputs. Aim to test edge cases and error conditions (e.g., if a
network call fails, the ViewModel should present an error state).
UI Tests use Xcode's UI Testing framework (built on XCTest) to
launch the app and simulate user interaction. You can record UI test
scripts by interacting with the app in the simulator, which
generates code for taps and swipes, or write them manually for more
control. Focus UI tests on critical flows, such as onboarding or a
purchase flow -- things that must work perfectly. Use assertions to
verify that expected elements appear or that navigating to a certain
screen is successful. Keep tests organized in groups and use
descriptive test method names. It's also helpful to run tests under
different configurations (Xcode's Test Plans can run a suite in
multiple schemes or environments). By automating testing, you can
catch regressions quickly and ensure new changes don't break
existing functionality. Make sure to run the test suite regularly
during development, and definitely include test execution as part of
your CI pipelines.
Continuous Integration with Xcode Cloud: To streamline building
and testing, set up Xcode Cloud if you host your code in a
supported git repository (GitHub, Bitbucket, GitLab, etc.). Xcode
Cloud is Apple's integrated CI service that can automatically build
your app on Apple's servers. To configure it, open your project in
Xcode, navigate to the Xcode Cloud settings (in the Report Navigator
or via Product ▸ Xcode Cloud) and enable a new workflow. Choose a
repository branch (e.g. main) and select actions like build,
analyze, test, and archive. You can specify triggers --
for example, run on every push, or only on pull requests. Xcode
Cloud will handle provisioning by linking with your Apple Developer
account; it can manage certificates and profiles for cloud builds
seamlessly if set to automatic signing. Add all your schemes that
need building/testing to the workflow (for instance, include both
iOS and macOS app schemes, and the test targets). Xcode Cloud
provides a simple web interface (or within Xcode) to monitor build
status and view logs or test results. You can also configure it to
automatically distribute successful builds to TestFlight (see step
10). One advantage of Xcode Cloud is deep integration: it uses the
same environment as local Xcode, and can run parallel tests on
multiple devices. Keep an eye on your Xcode Cloud minutes usage
(Apple provides some free tier, but heavy usage might require a
subscription). For team projects, Xcode Cloud ensures everyone's
changes are continuously validated. It's a great set-and-forget CI
for Apple platform apps, as long as your project is configured
properly.
Continuous Integration with GitHub Actions: As an alternative or
in addition to Xcode Cloud, you can use GitHub Actions to set up
CI/CD, which offers more customization. Create a workflow YAML (e.g.
.github/workflows/ci.yml) in your repository. Use a macOS
runner (e.g. runs-on: macos-latest) to build iOS/macOS apps. A
typical job installs dependencies, builds the app, runs tests, and
archives the app artifact. For example, include steps to check out
code (actions/checkout), set up any required Ruby or Node
environment (if using tools like Fastlane or CocoaPods), then run
Xcode build commands. You can use xcodebuild in command-line
mode to build and test:
- name: Build and Test (iOS)
run: xcodebuild -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' clean test
The above sample command builds the iOS scheme and runs tests on a
simulator. Similarly, you could build the macOS scheme by specifying the
scheme and destination as a Mac. If you have CocoaPods, remember to run
pod install before building. Save build artifacts if needed (Xcode
produces an archive .xcarchive and you can export an .ipa for iOS).
Using GitHub Secrets, you can store your distribution signing
certificate (as a Base64 .p12 file) and provisioning profile, as well as
App Store Connect API keys or an app-specific password. Then use a step
to install the certificate into the Keychain and environment variables
for signing. Many teams integrate Fastlane into GitHub Actions to
simplify code signing and uploading. For instance, after building, call
fastlane deliver or a custom lane to upload to TestFlight (Fastlane
can use the API key for App Store Connect to authenticate). There are
also community GitHub Actions (such as
apple-actions/app-store-connect) for uploading binaries to TestFlight.
Ensure your workflow runs on pull requests and merges to main, so that
every change is validated. With GitHub Actions, you have full control to
incorporate additional checks (like linting, SwiftLint, etc.) or
parallelize across matrix of devices/OS versions. It's a flexible
complement or alternative to Xcode Cloud, especially if you prefer
storing the CI config as code in your repo.
Manage Code Signing and Provisioning: Code signing is required
for running on devices and distributing via TestFlight/App Store.
Throughout development, Xcode's automatic signing can be enabled for
each target -- this ties the project to your Apple Developer Team
and will create the necessary certificates and provisioning profiles
for Debug and Release builds. Ensure each app target's Signing &
Capabilities has a team selected and a unique bundle identifier.
For distribution (TestFlight/App Store), you need an iOS
Distribution certificate and an App Store provisioning profile
for each app target. Xcode can create these if automatic signing is
on and the project's archive build is set to "Any iOS Device" (for
iOS) or "Any Mac" (for Mac apps). When using CI outside Xcode (like
GitHub Actions), you'll need to supply signing materials: export
your Distribution certificate as a .p12 file and download the
provisioning profile (.mobileprovision) from Apple Developer portal.
Store these securely (environment secrets or keychain in CI) and
have scripts or Fastlane import them during the build. A recommended
approach is to use Fastlane Match or codemagic/xcodesign to
manage signing identities in a secure, automated way. Also generate
an App Store Connect API Key (in App Store Connect > Users and
Access > Keys) if you plan to upload builds via API (used by CI
tools and Fastlane). Keep the key ID, issuer ID, and the private key
file secure; these allow CI to authenticate to App Store Connect
without requiring your Apple ID credentials. In summary, set up
signing early and test that you can archive and export the app
locally. This ensures that when CI tries to do the same, the process
is smooth. Maintaining consistent bundle IDs, provisioning profiles,
and entitlements across local and CI environments is critical.
Deploy to TestFlight: Once you have a signed archive build (an
.xcarchive), the next step is distributing it to testers.
TestFlight is Apple's beta distribution platform integrated with
App Store Connect. If using Xcode locally, go to Product ▸
Archive, then in the Organizer choose Distribute App → App
Store Connect → Upload. Xcode will handle the upload to
TestFlight (you'll need to increment the app version or build number
each time). For CI-based uploads, use either Xcode's command-line
tools or Fastlane. With Xcode command line, after archiving you can
export an IPA using xcodebuild -exportArchive (with an export
options plist), then upload with Apple's altool or the newer
Transporter CLI. Fastlane simplifies this via fastlane pilot
(for TestFlight) or fastlane upload_to_testflight. In Xcode Cloud,
enabling the "Upload to TestFlight" action in your workflow will
automatically push the archive to App Store Connect once the cloud
build succeeds. After the upload, App Store Connect will process the
build (this can take a few minutes). You can then log in to App
Store Connect to manage testers and send out the build to your
internal or external tester groups. It's good practice to annotate
what changes are in the build (TestFlight release notes) for your
testers. With CI/CD, you might choose to deploy every commit to an
internal TestFlight group for rapid iteration, and then promote
certain builds to external testers or App Store submission. Finally,
always monitor the TestFlight build for any critical issues
flagged by Apple (like crashes or missing compliance info) -- you'll
be notified in App Store Connect if something needs addressing. Once
a build is verified through testing, you can use it to submit the
app to the App Store for review.
Examples
- GitHub Actions CI Workflow (Excerpt): The following is a
simplified example of a GitHub Actions workflow file for an iOS app
that installs dependencies, builds, tests, and uploads a TestFlight
build. It demonstrates how to use Xcode command-line tools and
Fastlane in CI:
name: CI-iOS-TestFlight
on:
push:
branches: [ main ]
jobs:
build-test-deploy:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install CocoaPods dependencies
run: pod install
continue-on-error: true # Only if using CocoaPods
- name: Build and Run Unit Tests (iOS)
run: xcodebuild clean test -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -sdk iphonesimulator -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 14,OS=latest'
- name: Archive App for Distribution
run: xcodebuild clean archive -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -configuration Release -destination 'generic/platform=iOS' -archivePath ${{ github.workspace }}/YourApp.xcarchive
- name: Export .ipa from Archive
run: xcodebuild -exportArchive -archivePath ${{ github.workspace }}/YourApp.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath ${{ github.workspace }}/build
- name: Install Fastlane
run: gem install fastlane
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}
run: fastlane pilot upload -u ${{ secrets.APP_STORE_CONNECT_EMAIL }} -ipa ${{ github.workspace }}/build/YourApp.ipa --api_key_path ./ApiKeyFile.p8
In this YAML, the workflow triggers on pushes to the main branch. It
checks out the repository, installs pods (if applicable), then builds
and tests the app on an iOS simulator. Next it archives the app and
exports an IPA using an ExportOptions.plist (which would specify method
"app-store" and the provisioning profile). Finally, it uses Fastlane
Pilot to upload the IPA to TestFlight using App Store Connect API key
credentials stored in GitHub Secrets. This example can be extended with
additional jobs or steps for Mac builds, code linting, etc., and
illustrates how CI can fully automate the build and deploy process.
- MVVM ViewModel Example (SwiftUI): Below is a brief example of a
SwiftUI view and a ViewModel following the MVVM pattern. It shows how
a view model drives the UI state and handles logic, which could then
be unit-tested independently of the view:
import SwiftUI
import Combine
// Model
struct Game {
var score: Int
}
// ViewModel
class GameViewModel: ObservableObject {
@Published var game: Game
private var cancellables = Set<AnyCancellable>()
init(game: Game = Game(score: 0)) {
self.game = game
}
func increaseScore() {
game.score += 1
}
func resetScore() {
game.score = 0
}
}
// View
struct GameView: View {
@StateObject private var viewModel = GameViewModel()
var body: some View {
VStack {
Text("Score: \(viewModel.game.score)")
.font(.largeTitle)
HStack {
Button("Increase") {
viewModel.increaseScore()
}
Button("Reset") {
viewModel.resetScore()
}
}
}
.padding()
}
}
In this example, GameViewModel is an ObservableObject that manages
the state (the Game model). The SwiftUI GameView uses @StateObject
to instantiate and observe the ViewModel. Tapping the "Increase"
button calls a ViewModel method to update the score; thanks to
@Published, the view reflects the change automatically. This
architecture cleanly separates UI from logic: we can write unit tests
for GameViewModel.increaseScore() and resetScore() to ensure they
behave correctly without involving SwiftUI at all. The view simply
renders based on the current state. This pattern scales up such that for
each screen or component, you have a corresponding ViewModel (and
possibly service/model layers), making the app more maintainable and
testable.
References
- Apple Developer Documentation -- Multiplatform Apps: Guide on
configuring a single Xcode target for iOS and macOS, and sharing code
between platforms.
- Apple Developer Documentation -- Xcode Cloud: Overview of setting
up Xcode Cloud workflows for continuous integration and delivery of
apps (build, test, deploy with TestFlight).
- Apple Developer Documentation -- TestFlight Distribution:
Instructions for archiving an app and uploading builds to TestFlight
via Xcode or CI tools.
- Pointfree (Composable Architecture): Official GitHub repository
and documentation for The Composable Architecture (TCA) library,
including guides on integrating it into SwiftUI projects.
- XCTest Framework Reference: Apple's reference for writing unit and
UI tests with XCTest, including using
XCTAssert functions and UI
test recording.
- Fastlane Documentation: Guides for using Fastlane tools (
match,
pilot, etc.) to automate code signing and TestFlight deployments,
useful for setting up CI/CD pipelines outside Xcode Cloud.
1---2name: shared-swiftui-app-workflow3description: End-to-end Xcode workflow for architecting, debugging, profiling, and shipping a shared SwiftUI app on iOS and macOS.4---5
6# Instructions
7
8- **Overview:** This workflow outlines best practices to **build, test,
9 and deploy** a SwiftUI app targeting both iOS and macOS (with an
10 option for Catalyst). It covers project setup, architecture choices,
11 development practices, and continuous delivery steps.
12- **Prerequisites:** Ensure you have the latest Xcode installed (Xcode
13 15 or newer) and are enrolled in the Apple Developer Program (required
14 for code signing, Xcode Cloud, and TestFlight). Familiarity with
15 SwiftUI, Git source control, and basic iOS/macOS app development is
16 assumed.
17- **Usage:** Follow the steps below in sequence to configure a
18 multi-platform Xcode project, manage dependencies, implement an
19 architecture (MVVM or TCA), debug and profile efficiently, set up
20 CI/CD pipelines, and finally distribute the app via TestFlight.
21- **Conventions:** This guide uses **bold titles** for key actions and
22 *italics* for tool names or concepts. Replace example placeholders
23 (like bundle identifiers or scheme names) with your own
24 project-specific values when applying these steps.
25
26# Workflow
27
281. **Create a Multiplatform Xcode Project:** Begin by creating a new
29 Xcode project that supports both iOS and macOS targets. Xcode 14+
30 offers a **Multiplatform App** template, which sets up a single
31 target capable of building for iOS (and iPadOS) and macOS using
32 SwiftUI. This unified target shares most code and assets across
33 platforms. If you prefer separate targets, you can instead create an
34 iOS app and then add a macOS target (or enable Mac Catalyst for the
35 iOS target). Ensure that shared code (like SwiftUI views and models)
36 is grouped in a cross-platform group, and use platform checks
37 (`#if os(iOS)`, `#if os(macOS)`) for any platform-specific code. By
38 structuring the project as a **shared codebase**, you minimize
39 duplication while still tailoring the UI where necessary for each
40 device type.
41
422. **Set Up Targets, Schemes, and Configurations:** With a
43 multi-platform project, Xcode may already include separate
44 configurations for Debug and Release. You might add custom **Build
45 Configurations** (e.g. Staging or QA) if needed. If using separate
46 targets (for Catalyst or environment flavors), give each target a
47 unique **Bundle Identifier** and Info.plist. For example, suffix the
48 bundle ID with \".mac\" for a macOS target or \".dev\" for a
49 development build. Create corresponding **Schemes** for each app
50 target or environment so you can easily run and archive each
51 version. Mark schemes as "Shared" to include them in source control
52 (important for team use and CI). This multi-target setup allows you
53 to, for instance, have an iOS app, a native macOS app, and even a
54 Catalyst app all in one project, each with its own scheme and bundle
55 ID.
56
573. **Configure Environment Settings:** Manage environment-specific
58 settings by using Xcode's build configuration options. For example,
59 you can define custom **XCConfig** files or use **User-Defined Build
60 Settings** for values like API endpoints or feature flags. Define
61 keys in Info.plist that reference these settings. For instance, add
62 a key for `BaseURL` in your Info.plist and assign it a value like
63 `$(BASE_URL)` which is set per configuration. In Build Settings,
64 create a user-defined variable `BASE_URL` for each configuration
65 (e.g. Dev, QA, Prod), each pointing to the appropriate URL. Your app
66 can read these at runtime, for example:
67
68<!-- -->
69
70 let apiURL = Bundle.main.object(forInfoDictionaryKey: "BaseURL") as? String
71
72This way, you avoid hardcoding environment values. Additionally,
73consider using **Compiler Flags** for conditional code. In each
74configuration's build settings, you might add Swift flags like
75`-DDEVELOPMENT` or `-DPRODUCTION`. Then in Swift code, use
76`#if DEVELOPMENT` to include debug-only logic or use placeholders for
77testing. This approach keeps configuration differences isolated at build
78time. Finally, ensure each app target uses distinct app icons and names
79if needed (e.g. add suffix "Dev" to the app name for a development
80build) -- you can set this via Info.plist or Asset catalogs per target.
81
821. **Manage Dependencies (SPM and CocoaPods):** Use **Swift Package
83 Manager (SPM)** as the primary tool for adding libraries and
84 frameworks. SPM is built into Xcode, making dependency management
85 seamless for SwiftUI projects. To add a package, go to **File ▸ Add
86 Packages\...** and enter the package Git URL. Target the dependency
87 to your app target (and not to any CocoaPods-generated target) so
88 the package integrates correctly. SPM automatically fetches and
89 updates packages and keeps them sandboxed within Xcode. Commit the
90 `Package.resolved` file so that team members and CI use the same
91 versions. If you need a library that isn't available via SPM (or
92 contains significant Objective-C/legacy code), you can integrate
93 **CocoaPods**. Initialize a Podfile (`pod init`) and specify pods,
94 then run `pod install` to generate an `.xcworkspace`. Continue
95 working from the workspace thereafter. It's possible to mix SPM and
96 CocoaPods in one project -- just ensure that when adding SPM
97 packages you select your main project in the add dialog (not the
98 Pods project). Keep your Pod dependencies updated with `pod update`
99 as needed. In general, prefer SPM for pure Swift dependencies due to
100 its native Xcode support and ease of use, using CocoaPods only for
101 exceptions. Maintain clear documentation of third-party packages in
102 your README.
103
1042. **Apply an Architecture Pattern (MVVM or TCA):** Structure your
105 SwiftUI code using a robust architecture to manage complexity. A
106 popular choice is **MVVM (Model-View-ViewModel)**, which works
107 naturally with SwiftUI's data binding. In MVVM, define your data
108 models to represent app data, use SwiftUI Views for the UI, and
109 create **ViewModel** classes (conforming to `ObservableObject`) to
110 handle business logic and state. For example, a `GameViewModel`
111 might publish a `@Published var score` and handle methods to update
112 the score. The corresponding `GameView` uses `@StateObject` or
113 `@ObservedObject` to watch the ViewModel and update the UI. This
114 separation keeps the SwiftUI view declarative and lightweight, while
115 logic lives in the ViewModel (making it easier to test). For larger
116 apps or more complex state management, consider adopting **The
117 Composable Architecture (TCA)**. TCA is a library (addable via SPM)
118 that follows a unidirectional data flow (inspired by Redux). You
119 break down your app into **State**, **Actions**, and **Reducers**. A
120 Reducer is a pure function that takes the current State and an
121 Action and produces a new State (and optionally, side effects known
122 as Effects). A **Store** connects your SwiftUI View to the state and
123 business logic: the View sends Actions (for example, button taps),
124 which the Store receives and feeds into the Reducer, updating State
125 which then flows back to the View. TCA encourages a very modular
126 structure: you can compose small features into larger ones, and it
127 provides tools to manage dependencies and side effects in a
128 controlled way. While TCA has a learning curve, it excels in
129 testability (you can easily write tests for reducer logic) and
130 scalability for big apps. Choose either MVVM (simpler, uses
131 SwiftUI's built-in reactive state features) or TCA (more structured,
132 ideal for complex apps) depending on project needs -- both will help
133 maintain a clear separation of concerns in your code.
134
1353. **Debugging and Profiling Practices:** During development, use
136 Xcode's robust debugging tools to catch and fix issues early. Set
137 **breakpoints** in your code (by clicking the gutter next to a line
138 number) to pause execution and inspect variables at runtime. While
139 paused, use the **LLDB console** (`po` command) to print out values
140 or call functions to verify state. This is invaluable for logic in
141 ViewModels or TCA reducers where you want to ensure the correct data
142 flow. For SwiftUI views, the **View Hierarchy Debugger** is
143 extremely useful: run your app in Simulator and choose **Debug ▸
144 View Debugging ▸ Capture View Hierarchy**. This lets you inspect the
145 UI layout after a pause, so you can pinpoint why a view might not
146 appear or is misplaced. Additionally, leverage **Instruments** for
147 profiling. Xcode's Instruments app offers templates like **Time
148 Profiler** (to measure CPU performance and find slow functions) and
149 **Leaks** (to detect memory leaks and retain cycles). For example,
150 if a SwiftUI view is laggy, run Time Profiler while interacting with
151 it to identify expensive computations. SwiftUI in Xcode 15+ also
152 includes a dedicated "SwiftUI" animation and rendering timeline
153 instrument to analyze UI performance. Always test and profile in
154 **Release mode** periodically as well, since SwiftUI performance can
155 differ between Debug and Release builds. Use **os_log** or print
156 statements for lightweight debugging, especially to trace execution
157 paths or data changes (just be sure to remove or disable noisy logs
158 in production). By regularly debugging and profiling throughout
159 development, you ensure the app runs smoothly and catches bugs
160 before release.
161
1624. **Write Unit and UI Tests:** Set up automated tests to maintain code
163 quality. Xcode can generate a Unit Test target and a UI Test target
164 when you create the project (you can also add them manually via
165 **File ▸ New ▸ Target** if not present). **Unit Tests** (using the
166 XCTest framework) should cover your core business logic. For MVVM,
167 test your ViewModel methods and state changes independently of the
168 UI. For TCA, you can leverage the TCA testing utilities to send
169 actions to your reducers and assert on state changes or effect
170 outputs. Aim to test edge cases and error conditions (e.g., if a
171 network call fails, the ViewModel should present an error state).
172 **UI Tests** use Xcode's UI Testing framework (built on XCTest) to
173 launch the app and simulate user interaction. You can record UI test
174 scripts by interacting with the app in the simulator, which
175 generates code for taps and swipes, or write them manually for more
176 control. Focus UI tests on critical flows, such as onboarding or a
177 purchase flow -- things that must work perfectly. Use assertions to
178 verify that expected elements appear or that navigating to a certain
179 screen is successful. Keep tests organized in groups and use
180 descriptive test method names. It's also helpful to run tests under
181 different configurations (Xcode's Test Plans can run a suite in
182 multiple schemes or environments). By automating testing, you can
183 catch regressions quickly and ensure new changes don't break
184 existing functionality. Make sure to run the test suite regularly
185 during development, and definitely include test execution as part of
186 your CI pipelines.
187
1885. **Continuous Integration with Xcode Cloud:** To streamline building
189 and testing, set up **Xcode Cloud** if you host your code in a
190 supported git repository (GitHub, Bitbucket, GitLab, etc.). Xcode
191 Cloud is Apple's integrated CI service that can automatically build
192 your app on Apple's servers. To configure it, open your project in
193 Xcode, navigate to the Xcode Cloud settings (in the Report Navigator
194 or via Product ▸ Xcode Cloud) and enable a new workflow. Choose a
195 repository branch (e.g. main) and select actions like **build**,
196 **analyze**, **test**, and **archive**. You can specify triggers --
197 for example, run on every push, or only on pull requests. Xcode
198 Cloud will handle provisioning by linking with your Apple Developer
199 account; it can manage certificates and profiles for cloud builds
200 seamlessly if set to automatic signing. Add all your schemes that
201 need building/testing to the workflow (for instance, include both
202 iOS and macOS app schemes, and the test targets). Xcode Cloud
203 provides a simple web interface (or within Xcode) to monitor build
204 status and view logs or test results. You can also configure it to
205 automatically distribute successful builds to TestFlight (see step
206 10). One advantage of Xcode Cloud is deep integration: it uses the
207 same environment as local Xcode, and can run parallel tests on
208 multiple devices. Keep an eye on your Xcode Cloud minutes usage
209 (Apple provides some free tier, but heavy usage might require a
210 subscription). For team projects, Xcode Cloud ensures everyone's
211 changes are continuously validated. It's a great set-and-forget CI
212 for Apple platform apps, as long as your project is configured
213 properly.
214
2156. **Continuous Integration with GitHub Actions:** As an alternative or
216 in addition to Xcode Cloud, you can use **GitHub Actions** to set up
217 CI/CD, which offers more customization. Create a workflow YAML (e.g.
218 `.github/workflows/ci.yml`) in your repository. Use a **macOS
219 runner** (e.g. `runs-on: macos-latest`) to build iOS/macOS apps. A
220 typical job installs dependencies, builds the app, runs tests, and
221 archives the app artifact. For example, include steps to check out
222 code (`actions/checkout`), set up any required Ruby or Node
223 environment (if using tools like Fastlane or CocoaPods), then run
224 **Xcode build commands**. You can use `xcodebuild` in command-line
225 mode to build and test:
226
227<!-- -->
228
229 - name: Build and Test (iOS)
230 run: xcodebuild -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14' clean test
231
232The above sample command builds the iOS scheme and runs tests on a
233simulator. Similarly, you could build the macOS scheme by specifying the
234scheme and destination as a Mac. If you have CocoaPods, remember to run
235`pod install` before building. Save build artifacts if needed (Xcode
236produces an archive `.xcarchive` and you can export an `.ipa` for iOS).
237Using GitHub Secrets, you can store your distribution signing
238certificate (as a Base64 .p12 file) and provisioning profile, as well as
239App Store Connect API keys or an app-specific password. Then use a step
240to install the certificate into the Keychain and environment variables
241for signing. Many teams integrate **Fastlane** into GitHub Actions to
242simplify code signing and uploading. For instance, after building, call
243`fastlane deliver` or a custom lane to upload to TestFlight (Fastlane
244can use the API key for App Store Connect to authenticate). There are
245also community GitHub Actions (such as
246`apple-actions/app-store-connect`) for uploading binaries to TestFlight.
247Ensure your workflow runs on pull requests and merges to main, so that
248every change is validated. With GitHub Actions, you have full control to
249incorporate additional checks (like linting, SwiftLint, etc.) or
250parallelize across matrix of devices/OS versions. It's a flexible
251complement or alternative to Xcode Cloud, especially if you prefer
252storing the CI config as code in your repo.
253
2541. **Manage Code Signing and Provisioning:** Code signing is required
255 for running on devices and distributing via TestFlight/App Store.
256 Throughout development, Xcode's automatic signing can be enabled for
257 each target -- this ties the project to your Apple Developer Team
258 and will create the necessary certificates and provisioning profiles
259 for Debug and Release builds. Ensure each app target's **Signing &
260 Capabilities** has a team selected and a unique bundle identifier.
261 For distribution (TestFlight/App Store), you need an **iOS
262 Distribution certificate** and an **App Store provisioning profile**
263 for each app target. Xcode can create these if automatic signing is
264 on and the project's archive build is set to "Any iOS Device" (for
265 iOS) or "Any Mac" (for Mac apps). When using CI outside Xcode (like
266 GitHub Actions), you'll need to supply signing materials: export
267 your Distribution certificate as a `.p12` file and download the
268 provisioning profile (.mobileprovision) from Apple Developer portal.
269 Store these securely (environment secrets or keychain in CI) and
270 have scripts or Fastlane import them during the build. A recommended
271 approach is to use **Fastlane Match** or **codemagic/xcodesign** to
272 manage signing identities in a secure, automated way. Also generate
273 an **App Store Connect API Key** (in App Store Connect \> Users and
274 Access \> Keys) if you plan to upload builds via API (used by CI
275 tools and Fastlane). Keep the key ID, issuer ID, and the private key
276 file secure; these allow CI to authenticate to App Store Connect
277 without requiring your Apple ID credentials. In summary, set up
278 signing early and test that you can archive and export the app
279 locally. This ensures that when CI tries to do the same, the process
280 is smooth. Maintaining consistent bundle IDs, provisioning profiles,
281 and entitlements across local and CI environments is critical.
282
2832. **Deploy to TestFlight:** Once you have a signed archive build (an
284 *.xcarchive*), the next step is distributing it to testers.
285 **TestFlight** is Apple's beta distribution platform integrated with
286 App Store Connect. If using Xcode locally, go to **Product ▸
287 Archive**, then in the Organizer choose **Distribute App** → **App
288 Store Connect** → **Upload**. Xcode will handle the upload to
289 TestFlight (you'll need to increment the app version or build number
290 each time). For CI-based uploads, use either Xcode's command-line
291 tools or Fastlane. With Xcode command line, after archiving you can
292 export an IPA using `xcodebuild -exportArchive` (with an export
293 options plist), then upload with Apple's **altool** or the newer
294 **Transporter** CLI. Fastlane simplifies this via `fastlane pilot`
295 (for TestFlight) or `fastlane upload_to_testflight`. In Xcode Cloud,
296 enabling the "Upload to TestFlight" action in your workflow will
297 automatically push the archive to App Store Connect once the cloud
298 build succeeds. After the upload, App Store Connect will process the
299 build (this can take a few minutes). You can then log in to App
300 Store Connect to manage testers and send out the build to your
301 internal or external tester groups. It's good practice to annotate
302 what changes are in the build (TestFlight release notes) for your
303 testers. With CI/CD, you might choose to deploy every commit to an
304 internal TestFlight group for rapid iteration, and then promote
305 certain builds to external testers or App Store submission. Finally,
306 always monitor the TestFlight build for any **critical issues**
307 flagged by Apple (like crashes or missing compliance info) -- you'll
308 be notified in App Store Connect if something needs addressing. Once
309 a build is verified through testing, you can use it to submit the
310 app to the App Store for review.
311
312# Examples
313
314- **GitHub Actions CI Workflow (Excerpt):** The following is a
315 simplified example of a GitHub Actions workflow file for an iOS app
316 that installs dependencies, builds, tests, and uploads a TestFlight
317 build. It demonstrates how to use Xcode command-line tools and
318 Fastlane in CI:
319
320<!-- -->
321
322 name: CI-iOS-TestFlight
323 on:
324 push:
325 branches: [ main ]
326 jobs:
327 build-test-deploy:
328 runs-on: macos-latest
329 steps:
330 - name: Checkout code
331 uses: actions/checkout@v3
332
333 - name: Install CocoaPods dependencies
334 run: pod install
335 continue-on-error: true # Only if using CocoaPods
336
337 - name: Build and Run Unit Tests (iOS)
338 run: xcodebuild clean test -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -sdk iphonesimulator -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 14,OS=latest'
339
340 - name: Archive App for Distribution
341 run: xcodebuild clean archive -workspace YourApp.xcworkspace -scheme "YourApp-iOS" -configuration Release -destination 'generic/platform=iOS' -archivePath ${{ github.workspace }}/YourApp.xcarchive
342
343 - name: Export .ipa from Archive
344 run: xcodebuild -exportArchive -archivePath ${{ github.workspace }}/YourApp.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath ${{ github.workspace }}/build
345
346 - name: Install Fastlane
347 run: gem install fastlane
348
349 - name: Upload to TestFlight
350 env:
351 APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
352 APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
353 APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}
354 run: fastlane pilot upload -u ${{ secrets.APP_STORE_CONNECT_EMAIL }} -ipa ${{ github.workspace }}/build/YourApp.ipa --api_key_path ./ApiKeyFile.p8
355
356In this YAML, the workflow triggers on pushes to the main branch. It
357checks out the repository, installs pods (if applicable), then builds
358and tests the app on an iOS simulator. Next it archives the app and
359exports an IPA using an ExportOptions.plist (which would specify method
360\"app-store\" and the provisioning profile). Finally, it uses Fastlane
361Pilot to upload the IPA to TestFlight using App Store Connect API key
362credentials stored in GitHub Secrets. This example can be extended with
363additional jobs or steps for Mac builds, code linting, etc., and
364illustrates how CI can fully automate the build and deploy process.
365
366- **MVVM ViewModel Example (SwiftUI):** Below is a brief example of a
367 SwiftUI view and a ViewModel following the MVVM pattern. It shows how
368 a view model drives the UI state and handles logic, which could then
369 be unit-tested independently of the view:
370
371<!-- -->
372
373 import SwiftUI
374 import Combine
375
376 // Model
377 struct Game {
378 var score: Int
379 }
380
381 // ViewModel
382 class GameViewModel: ObservableObject {
383 @Published var game: Game
384 private var cancellables = Set<AnyCancellable>()
385
386 init(game: Game = Game(score: 0)) {
387 self.game = game
388 }
389
390 func increaseScore() {
391 game.score += 1
392 }
393
394 func resetScore() {
395 game.score = 0
396 }
397 }
398
399 // View
400 struct GameView: View {
401 @StateObject private var viewModel = GameViewModel()
402
403 var body: some View {
404 VStack {
405 Text("Score: \(viewModel.game.score)")
406 .font(.largeTitle)
407 HStack {
408 Button("Increase") {
409 viewModel.increaseScore()
410 }
411 Button("Reset") {
412 viewModel.resetScore()
413 }
414 }
415 }
416 .padding()
417 }
418 }
419
420In this example, `GameViewModel` is an `ObservableObject` that manages
421the state (the `Game` model). The SwiftUI `GameView` uses `@StateObject`
422to instantiate and observe the ViewModel. Tapping the \"Increase\"
423button calls a ViewModel method to update the score; thanks to
424`@Published`, the view reflects the change automatically. This
425architecture cleanly separates UI from logic: we can write unit tests
426for `GameViewModel.increaseScore()` and `resetScore()` to ensure they
427behave correctly without involving SwiftUI at all. The view simply
428renders based on the current state. This pattern scales up such that for
429each screen or component, you have a corresponding ViewModel (and
430possibly service/model layers), making the app more maintainable and
431testable.
432
433# References
434
435- Apple Developer Documentation -- **Multiplatform Apps:** Guide on
436 configuring a single Xcode target for iOS and macOS, and sharing code
437 between platforms.
438- Apple Developer Documentation -- **Xcode Cloud:** Overview of setting
439 up Xcode Cloud workflows for continuous integration and delivery of
440 apps (build, test, deploy with TestFlight).
441- Apple Developer Documentation -- **TestFlight Distribution:**
442 Instructions for archiving an app and uploading builds to TestFlight
443 via Xcode or CI tools.
444- **Pointfree (Composable Architecture):** Official GitHub repository
445 and documentation for The Composable Architecture (TCA) library,
446 including guides on integrating it into SwiftUI projects.
447- **XCTest Framework Reference:** Apple's reference for writing unit and
448 UI tests with XCTest, including using `XCTAssert` functions and UI
449 test recording.
450- **Fastlane Documentation:** Guides for using Fastlane tools (`match`,
451 `pilot`, etc.) to automate code signing and TestFlight deployments,
452 useful for setting up CI/CD pipelines outside Xcode Cloud.
453
454------------------------------------------------------------------------