Exploring Cursor: Script to Add Files to Xcode From Composer

When working with Compose and Xcode, the newly created files from Composer do not show up in Xcode, and you have to add the reference manually. On the path to exploring Cursor, let's automate that process, keeping things as simple as possible.

Thanks to Mike Pyrka for providing the initial script I updated for my needs!

Project to a Group in Xcode 16

If you are using Xcode 16, first convert the project into a group. This step is required because Xcodeproj has not been updated to handle the new folder system. This step makes it easier for the script to add files later.

If the project was created using Xcode 16, select the project's .xcodeproj file and "Show Package Contents." Open the project’s .pbxproj file, search for objectVersion = 77;, and replace it with objectVersion = 63;.

This step is required for the same reason as above. Once it is updated, this step might not be needed, but it is a must for now.

Install xcodeproj Gem

You will need to install the xcodeproj gem if you have not done so already. This gem helps to interact with Xcode projects through Ruby. Open terminal and run the following command:

gem install xcodeproj

Script to Add Files to Xcode

Here is the script in Ruby to add files to Xcode:

#!/usr/bin/env ruby

require 'xcodeproj'

# Find the .xcodeproj file in the current directory
project_path = Dir.glob('./*.xcodeproj').first

unless project_path
  puts "Error: No .xcodeproj file found in the current directory."
  exit 1
end

puts "Found project: #{project_path}"

# Open the Xcode project
project = Xcodeproj::Project.open(project_path)

# Find the main group (usually named after your project)
main_group = project.root_object.main_group
puts "Main group: #{main_group.display_name}"

# Get the main target (assuming you want the first target)
main_target = project.targets.first
puts "Main target: #{main_target.name}"

# Read the content of the pbxproj file
pbxproj_path = File.join(project_path, 'project.pbxproj')
pbxproj_content = File.read(pbxproj_path)

# Get all Swift files in the project directory and its subdirectories
swift_files = Dir.glob(File.join(File.dirname(project_path), '**', '*.swift'))
puts "Found #{swift_files.length} Swift files"

# Function to check if a file is in pbxproj
def file_in_pbxproj?(file_name, pbxproj_content)
  pbxproj_content.include?(file_name)
end

# Function to find or create nested groups
def find_or_create_group(parent_group, group_path)
  group_path.split('/').inject(parent_group) do |current_group, group_name|
    current_group.children.find { |child| child.is_a?(Xcodeproj::Project::Object::PBXGroup) && child.display_name == group_name } ||
      current_group.new_group(group_name)
  end
end

# Add new files to the project
swift_files.each do |file_path|
  file_name = File.basename(file_path)
  relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(project_path).parent).to_s
  
  unless file_in_pbxproj?(file_name, pbxproj_content)
    puts "Adding file: #{relative_path}"
    
    # Find or create nested groups based on the file path
    group_path = File.dirname(relative_path)
    target_group = find_or_create_group(main_group, group_path)
    
    file_ref = target_group.new_reference(relative_path)
    
    # Ensure the path is relative to the group
    file_ref.set_path(File.basename(relative_path))
    file_ref.source_tree = '<group>'
    
    # Add file reference to the target
    main_target.add_file_references([file_ref])
    
    # Ensure the file is included in the appropriate build phases
    main_target.source_build_phase.add_file_reference(file_ref) unless main_target.source_build_phase.file_display_names.include?(file_name)
  else
    puts "File already exists in project: #{relative_path}"
  end
end

# Save the project
project.save

puts "Project updated and saved."

This Ruby script automates adding Swift files to the Xcode project. Instead of manually dragging files into Xcode, this script handles everything for you by modifying the .xcodeproj file directly.

#!/usr/bin/env ruby
require 'xcodeproj'
  • #!/usr/bin/env ruby: This line tells the system to use Ruby to execute the script.
  • require 'xcodeproj': This imports the xcodeproj gem, which allows to interact with Xcode project files.
project_path = Dir.glob('./*.xcodeproj').first

unless project_path
  puts "Error: No .xcodeproj file found in the current directory."
  exit 1
end
puts "Found project: #{project_path}"
  • Dir.glob('./*.xcodeproj').first: Searches the current directory for a file with the .xcodeproj extension.
project = Xcodeproj::Project.open(project_path)
main_group = project.root_object.main_group
puts "Main group: #{main_group.display_name}"
main_target = project.targets.first
puts "Main target: #{main_target.name}"
  • Xcodeproj::Project.open(project_path): Opens the Xcode project file.
  • project.root_object.main_group: Retrieves the main group, representing the project's top-level folder.
  • project.targets.first: Assume that you want to work with the first target in your project (usually the main target).
pbxproj_path = File.join(project_path, 'project.pbxproj')
pbxproj_content = File.read(pbxproj_path)
  • project.pbxproj contains all the details of the Xcode project configuration. This reads its content into pbxproj_content.
swift_files = Dir.glob(File.join(File.dirname(project_path), '**', '*.swift'))
puts "Found #{swift_files.length} Swift files"
  • This line searches for all .swift files within the project directory and its subdirectories, saving them in swift_files.
def file_in_pbxproj?(file_name, pbxproj_content)
  pbxproj_content.include?(file_name)
end
  • file_in_pbxproj?: This function checks if a file is already part of the pbxproj file by searching for its name within pbxproj_content.
def find_or_create_group(parent_group, group_path)
  group_path.split('/').inject(parent_group) do |current_group, group_name|
    current_group.children.find { |child| child.is_a?(Xcodeproj::Project::Object::PBXGroup) && child.display_name == group_name } ||
      current_group.new_group(group_name)
  end
end
  • This function splits the file path into individual folder names, and either finds or creates these folders within the project, ensuring the file is added to the correct location.
swift_files.each do |file_path|
  file_name = File.basename(file_path)
  relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(project_path).parent).to_s
  
  unless file_in_pbxproj?(file_name, pbxproj_content)
    puts "Adding file: #{relative_path}"
    
    group_path = File.dirname(relative_path)
    target_group = find_or_create_group(main_group, group_path)
    
    file_ref = target_group.new_reference(relative_path)
    file_ref.set_path(File.basename(relative_path))
    file_ref.source_tree = '<group>'
    
    main_target.add_file_references([file_ref])
    main_target.source_build_phase.add_file_reference(file_ref) unless main_target.source_build_phase.file_display_names.include?(file_name)
  else
    puts "File already exists in project: #{relative_path}"
  end
end
  • Loops through each Swift file:
    • Checks if the file is already in the project using file_in_pbxproj?.
    • If not, it finds or creates the group/folder structure, adds the file reference, and make sure it is included in the appropriate build phase of the target.
project.save
puts "Project updated and saved."
  • Saves all the changes to the .xcodeproj file, to confirm the new files are now part of the Xcode project.

Now, create a new fill with this Ruby script that will add files to Xcode. Save it as add_files_to_xcode.rb. To make it executable, run this command in the terminal:

chmod +x add_files_to_xcode.rb

This command changes the script's permissions, allowing you to execute it.

Set Up Cursor with launch.json

Next, open Cursor. Create a new folder named .vscode in the directory. Inside this folder, create a file called launch.json with the following configuration:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Build in Xcode",
      "type": "node-terminal",
      "request": "launch",
      "command": "/Users/rudrank/Downloads/add_files_to_xcode.rb", // use a better folder path
      "cwd": "${workspaceFolder}",
      "presentation": {
        "reveal": "never",
        "panel": "dedicated"
      }
    }
  ]
}

Update the "command" path to where you saved the add_files_to_xcode.rb file. This launch.json configuration tells Cursor how to run the Ruby script from within the IDE.

When you add a new file using Composer and build in Xcode, running the script will automatically add those files to your Xcode project. You won’t have to drag and drop files manually anymore!

Moving Forward

Currently, my script only handles files in the root directory, which can be a bit limiting for larger projects. I am looking into improving this to handle more complex folder structures.

Feel free to tweak things as you need, and happy cursoring!