Claude
Skills
Sign in
Back

Shared SwiftUI App Workflow

Included with Lifetime
$97 forever

End-to-end Xcode workflow for architecting, debugging, profiling, and shipping a shared SwiftUI app on iOS and macOS.

Generalxcodeswiftuiworkflowcicdmultiplatform

What this skill does


# 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

1.  **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.

2.  **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.

3.  **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.

1.  **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.

2.  **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.

3.  **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. 

Related in General