Build a CLI that fetches files for your Xcode project !

Hi guys ! I’m back with a short article on how I got in a situation where I needed to programmatically add files to a Xcode project.

As you may know, I come from the web environment where everything is highly customizable with a lot of resources available on the web on all possible topics making almost everything accessible and simple.

But this time, I was in unknown domains as I left the web environment to enter in the mobile universe. My quest was simple: I needed to build a simple iOS project that displays simple components.

But, the components files that I wanted to display had to be fetched by a CLI (which I also had to create). I though this would be easy to do given that I used to make such programs for the web with node.js scripts.

So naively, I built my CLI as if I were in a web environment. I wrote a basic node.js script that fetches files and writes them on the disk but I quickly saw that something was not working as expected. The files that were fetched and written on the disk were not showing in my Xcode Project.

After some research on the web, I found that it isn’t that easy to add files programmatically to an iOS project.

If you pay attention to your Git diff after adding a file to your Xcode project using the normal way (“New File…” in Xcode), you will see that your file is visible and added to your hard drive but that there are also two other files that have been modified.

Theses files are project configuration files that are difficult to read from a human and required by Xcode to build the project normally. Unfortunately, as they are complex to understand, it can be risky or even impossible to edit them manually.

So how do we do ?

Fortunately, I wasn’t the only one on this planet who needed to modify my Xcode project programmatically, and in order to meet this need, the CocoaPods project team created an awesome project called xcodeproj which is a library that exposes functions that allow you to manipulate Xcode Project. The project is a ruby gem, which means I had to rewrite my node.js script in ruby in order to use it.

Note that there is an other project very similar to xcodeproj but in python called mod-pbxproj.

Now let me show you how we add files and groups to a Xcode Project using xcodeproj.

First, we need to get the project object, which is the entry point of every manipulation :

require 'xcodeproj'

$project = Xcodeproj::Project.open("./pathOfYourProjectFile/myproject.xcodeproj")

Then, let’s create a group (a virtual folder in a Xcode Project) :

require 'fileutils'
require 'xcodeproj'

newFolderName = "my-new-folder"
newFolderPath = File.join($project.path, "./" + newFolderName)

# 1. We create a real folder on the disk
FileUtils.mkdir_p(newFolderPath)

# 2. We create the associated project group at the same path
newFolderGroup = $project.new_group(newFolderName)

# 3. Save changes
$project.save("./pathOfYourProjectFile/myproject.xcodeproj")

Once the group is created, let’s add a file to it:

require 'xcodeproj'

newFileName = "my-new-file.swift"
newFilePath = newFolderPath + "/" + newFilePath

# 1. We create the real file on the disk
begin
    File.write(newFilePath, fileContent)
rescue => e
    STDERR.puts "[ERROR] => " + e
end

# 2. Then we add the reference of our created file to our previously created group
createdFile = newFolderGroup.new_reference(newFilePath)

# 3. Add the reference of the file to you project first target
$project.targets.first.add_file_references([createdFile])

# 4. Save changes
$project.save("./pathOfYourProjectFile/myproject.xcodeproj")

Look that after our manipulations are done, we need to save the changes :

$project.save("./pathOfYourProjectFile/myproject.xcodeproj")

And that’s it ! It’s not more complicated than that. If you are not used to ruby programs, here is the shell command needed to execute a Ruby script :

ruby myscript.rb

Once my script was working as expected, I wanted this script to run automatically.

Usually, in web, when I need to add resources to a project from a script, I want the script to be triggered after installing the dependencies.

In order to have the same workflow, I installed Cocoapods which is a dependency manager for Xcode Projects. This dependency manager exposes a hook to run a script when it has finished installing packages.

In order to do that, we need to update our `Podfile` :

# put this snippet at the end of your Podfile 
post_install do |installer| 
  system("ruby my script.rb") 
end

And here we are, now we have set up a script that fetches files and adds it to our Xcode Project which automatically trigger when we try to install the project dependencies like we would in other types of projects.

I hope this article saves you a lot of trouble from doing such things :p

Happy Coding

JG

Leave a Reply

Your email address will not be published. Required fields are marked *