Tutorials

How to sync Cursor AI and Swift XCode projects (xcodeproj)

Create a boilerplate for your next Swift project with Cursor AI and XcodeGen

Introduction

Xcode uses .xcodeproj files to manage everything. It tracks file references, groups, and build settings all in one place.

But the problem is that when you make changes outside of Xcode, such as in VSCode or with Cursor AI, those changes don't automatically reflect in the Xcode project.

You add a new Swift file in VSCode, and it’s like Xcode has no idea it exists. If you’re not careful, you can end up with mismatched files, missing references, or duplicate folders.

Let’s break down how you can keep everything in harmony, step by step.

1. Setting Up Your Swift Environment in Cursor AI

Installing Swift Extensions in Cursor

Swift Extension

First, make sure Swift support is fully set up in Cursor AI:

  • Syntax highlighting and code completion
  • Code navigation features
  • Refactoring and quick fixes
  • Package management support
  • Debugging integration

Install Swift Extension

SwiftLint

While not strictly necessary, this tool helps enforce Swift style and conventions.

brew install swiftlint

Install Swiftlint VS Code Extension

Swiftlint Docs

CodeLLDB

This extension is for debugging Swift code. It integrates with LLDB to provide a rich debugging experience.

Install CodeLLDB

Add a cursorrule file

With .cursorrules we can give instructions to the AI.

This helps the AI to understand how we want our code and descriptions to be written.

Start with adding a .cursorrules file to the root directory of your project.

Here is an example of what you can add:

you are an expert in coding with swift, swift ui. you always write maintainable code and clean code.

focus on latest september 2024 versions of the documentation and features.

your descriptions should be short and concise.
don't remove any comments.

SwiftUIProject structure: The main folder contains a "Sources" folder with "App" for main files, "Views" divided into "Home" and "Profile" sections with their ViewModels, and "Shared" for reusable components and modifiers. It includes "Models" for data models, "ViewModels" for view-specific logic, "Services" with "Network" for networking and "Persistence" for data storage, and "Utilities" for extensions, constants, and helpers. The "Resources" folder holds "Assets" for images and colors, "Localization" for localized strings, and "Fonts" for custom fonts. Lastly, the "Tests" folder includes "UnitTests" for unit testing and "UITests" for UI testing.

SwiftUI UI Design Rules:

Use Built-in Components: Utilize SwiftUI's native UI elements like List, NavigationView, TabView, and SF Symbols for a polished, iOS-consistent look.
Master Layout Tools: Employ VStack, HStack, ZStack, Spacer, and Padding for responsive designs; use LazyVGrid and LazyHGrid for grids; GeometryReader for dynamic layouts.
Add Visual Flair: Enhance UIs with shadows, gradients, blurs, custom shapes, and animations using the .animation() modifier for smooth transitions.
Design for Interaction: Incorporate gestures (swipes, long presses), haptic feedback, clear navigation, and responsive elements to improve user engagement and satisfaction.

Add swift documentation

Consider adding some custom swift documentation to your project. You can add any docs you like in cursor under

Cursor > Preferences > Cursor Settings > Features.

2. Install Sync and Code Generation libraries

As we learned in the previous section, XCode has its quirks. Add a file in Cursor? Rename something? Xcode ignores you.

Introducing XcodeGen

XcodeGen makes sure your project files stay in line with your actual folder structure. It's a real time-saver and keeps your workflow smooth.

Install XcodeGen

First things first, let's get XcodeGen installed. If you’ve got Homebrew, you’re set:

brew install xcodegen

Create your project.yml

This file is the heart of XcodeGen. It defines your project structure, targets, and settings. Here’s a basic example to get you going:

project.yml
name: MyZunderApp
options:
  bundleIdPrefix: com.zunderai
  deploymentTarget:
    iOS: 17.0
  xcodeVersion: "15.3"
  generateEmptyDirectories: true
  createIntermediateGroups: true

targets:
  MyZunderApp:
    type: application
    platform: iOS
    sources: [MyZunderApp]
    settings:
      base:
        SWIFT_VERSION: 5.10.1
        ENABLE_TESTABILITY: YES
    info:
      path: Sources/Info.plist
      properties:
        CFBundleShortVersionString: "1.0.0"
        CFBundleVersion: "1"
        UILaunchStoryboardName: LaunchScreen
        UIApplicationSceneManifest:
          UIApplicationSupportsMultipleScenes: true

schemes:
  MyZunderApp:
    build:
      targets:
        MyZunderApp: all
    run:
      config: Debug

configs:
  Debug: debug
  Release: release
Make sure to replace MyZunderApp with your project name.

Create the project dir

If the project.yml is like the following:

project.yml
name: MyZunderApp

Then the project directory should be like the following:

MyZunderApp/

If you don't create this folder the xcodegen generate command will not work.

mkdir MyZunderApp

Generate your XCode project

xcodegen generate

3. Automate Commands for a better workflow

In the next step we want to improve the workflow to reduce manual steps.

We will create a tasks.json file and automate essential commands.

Like generating the XCode project, building the project or running Swiftlint.

So with this configuration we maintain consistency, and keep everything in sync without leaving your coding environment.

Create a new file in .vscode/tasks.json and add the following:

tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Generate Xcode Project with XcodeGen",
            "type": "shell",
            "command": "xcodegen",
            "args": [
                "generate"
            ],
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "runOptions": {
                "runOn": "folderOpen"
            }
        },
        {
            "label": "Build Swift Project",
            "type": "shell",
            "command": "xcodebuild",
            "args": [
            "-project",
                "todoapp.xcodeproj",
                "-scheme",
                "MyZunderApp",
                "-configuration",
                "Debug",
                "clean",
                "build"
            ],
            "problemMatcher": [
                "$xcodebuild"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared",
                "showReuseMessage": true,
                "clear": false
            },
            "runOptions": {
                "runOn": "default"
            }
        }
    ]
}
Make sure to replace -project and -scheme args with your project and scheme name. Look at the project.yml file to find out what your project and scheme name is.
  "-project",
  "todoapp.xcodeproj", # <-- replace this with your project name
  "-scheme",
  "MyZunderApp", # <-- replace this with your scheme name

Run tasks with Cursor AI

Now you can run the tasks with Cursor AI.

Hit (Cmd+Shift+B or Ctrl+Shift+B) and select the task you want to run.

Bonus: Use swiftlint to keep your code clean

Install Swiftlint

If you haven't already installed swiftlint.

brew install swiftlint

Add a Swiftlint configuration file

In your project directory, create a .swiftlint.yml file to customize SwiftLint rules according to your needs:

.swiftlint.yml
disabled_rules:
  - line_length              # Disable if long lines are frequently unavoidable
  - trailing_whitespace      # Manage this rule based on team preferences
  - file_length              # Disable for larger files like data models
  - cyclomatic_complexity    # Disable if complex functions are unavoidable
  - force_cast               # Be cautious but allow force casting where necessary
  - force_unwrapping         # Allow force unwrapping for simplicity in some cases
  - large_tuple              # Manage tuple size based on specific needs
  - function_body_length     # Disable if longer functions are necessary
  - type_body_length         # Disable if types naturally grow large

opt_in_rules:
  - empty_count              # Prefer `.isEmpty` over counting elements
  - closure_end_indentation  # Align closing brackets for better readability
  - sorted_first_last        # Use `.first` and `.last` instead of `.sorted()`
  - strict_fileprivate       # Enforce using `fileprivate` when appropriate
  - redundant_nil_coalescing # Avoid unnecessary nil coalescing

included:
  - Sources
  - Tests

excluded:
  - Carthage
  - Pods
  - .build
  - Generated

reporter: "xcode"

Integrate Swiftlint to Cursor AI

Add this to your .vscode/tasks.json file:

tasks.json
{
  "label": "Run SwiftLint",
  "type": "shell",
  "command": "swiftlint",
  "args": [],
  "problemMatcher": [
      "$swiftlint"
  ],
  "group": {
      "kind": "build",
      "isDefault": true
  },
  "presentation": {
      "echo": true,
      "reveal": "always",
      "focus": false,
      "panel": "shared",
      "showReuseMessage": true,
      "clear": false
  },
  "runOptions": {
      "runOn": "folderOpen"
  }
}

Now you can run the Swiftlint command from the command palette in Cursor.

Hit (Cmd+Shift+B or Ctrl+Shift+B) and select "Run Task" > "Run SwiftLint".

More resources

Swiftenv

swiftenv is a tool to easily manage and switch between multiple Swift versions on your system.

Happy coding! 🎉