Share Xcode Schemes

So you are facing one of these Xcode errors where it tells you that there’s no such scheme? This post could help to explain why Xcode does it and how to solve this problem.

Shared vs User Schemes

While setting up build server for iOS app I have faced this issue multitude of times. Xcode schemes can be either shared or not. By default schemes are not shared and are owned by a user that creates them.

Here’s how it looks in Xcode, note the last column with checkboxes. Manage Schemes

If you look inside kartoteka-reloaded.xcodeproj folder you will see how Xcode stores the schemes. Here’s how it looks when kartoteka-reloaded.xcscheme is not shared

kartoteka-reloaded.xcodeproj/
├── project.pbxproj
├── project.xcworkspace
│   └── contents.xcworkspacedata
├── xcshareddata
│   └── xcschemes
└── xcuserdata
    └── grebenetsm.xcuserdatad
        └── xcschemes
            ├── kartoteka-reloaded.xcscheme
            └── xcschememanagement.plist

Now let’s tick the “Shared” checkbox then list directory contents again

kartoteka-reloaded.xcodeproj/
├── project.pbxproj
├── project.xcworkspace
│   └── contents.xcworkspacedata
├── xcshareddata
│   └── xcschemes
│       └── kartoteka-reloaded.xcscheme
└── xcuserdata
    └── grebenetsm.xcuserdatad
        └── xcschemes
            └── xcschememanagement.plist

Notice how the kartoteka-reloaded.xcscheme moved from user data folder to shared data folder. This is basically what makes scheme a shared one.

The general practice for Xcode projects .gitignore file is to ignore user data

# .gitignore
xcuserdata/
*.xcuserdatad

So when you check out source code on a build box, there won’t be any user schemes inside .xcodeproj folder and xcodebuild won’t be able to see the schemes and will fail to build them.

Solution

You can solve this problem either manually or automatically.

Of course you can just talk to devs and ask them to share the scheme. Done! You can even do this change yourself and create pull request with changes.

But this approach will not work in some cases. For example, if you want to run UI automation tests with Calabash, the steps are

  • Duplicate existing Xcode target and name new test target with -cal suffix
  • Add Calabash framework to test target
  • Build -cal test target and run tests

The first step is done with calabash-ios setup command. When new target is created a scheme is created for it as well. This is the default setting for all Xcode projects and in this post we’ll assume that’s the way you have it configured as well.

Now the tricky part, it doesn’t matter if original scheme was shared, the new -cal scheme will not be shared. That means you won’t be able to build it from command line.

Since it all happens on a build box as part of a build plan, you can’t push anything back to the repository, you have to find a way to make this new scheme shared right now.

The answer to your problems comes from Ruby world. In particular the xcodeproj Ruby gem. This is an incredibly handy library to work with Xcode projects and workspaces. You can do pretty much anything you need, create and modify targets and schemes, add new files to targets, modify build settings and other properties, and, of course, share schemes. By the way, xcodeproj is used by CocoaPods and that says a lot.

Go ahead and install the gem

[sudo] gem install xcodeproj

Now create a simple Ruby file, name it whatever you want

#!/usr/bin/env ruby
# share_schemes.rb

require 'xcodeproj'
xcproj = Xcodeproj::Project.open("MyProject.xcodeproj")
xcproj.recreate_user_schemes
xcproj.save

This is it! Put your Xcode project name in there, then run and the scheme will be shared.

chmod +x share_schemes.rb
./share_schemes.rb

Caveats

It sounds to good to be true, right? There’s a number of situations where sharing a scheme via Ruby script will not work as expected.

If your Xcode project already has a shared scheme, then you will end up having one scheme from user’s data directory and another one form xcshareddata. Xcode IDE will pick up both and that’s the reason why you see same scheme twice with project name in parentheses. Manage Schemes

That’s not very bad and doesn’t normally cause any problems. Until that moment of time when you modify one scheme and forget about another. The best way to avoid this problem is to share schemes from day 1, Xcode will not create user schemes then.

The real trouble begins if you didn’t have any shared schemes and the scheme that you want to recreate and share is linked to a test target. That’s the default configuration for unit tests. So the problem is that xcodeproj doesn’t recreate dependencies to test target. If you run a xcodebuild test action you’ll be surprised to see it failing. Unfortunately there isn’t an easy workaround for this problem, so you’d better share those schemes manually and commit changes to source control system.

Summary

Surely the Ruby script can be improved, you’d want to pass Xcode project name as an argument or even look it up automatically.

As usual, in Summary I just provide file listing with a solution ready to copy-paste. Here’s more advanced Ruby script for your use. You can just get the file directly if you’d like, share-schemes.rb

#!/usr/bin/env ruby
# share_schemes.rb

require 'optparse'
require 'ostruct'
require 'rubygems'
require 'xcodeproj'
require 'colorize'
require 'fileutils'

# Option parser
class OptionParser

    # Parse options
    # @param [Array<String>] args command line args
    # @return [OpenStruct] parsed options
    def self.parse(args)
        options = OpenStruct.new
        options.project = nil

        opt_parser = OptionParser.new do |opts|

            opts.banner = "Usage: #{File.basename($0)} [options]"

            opts.separator("")
            opts.on('-p [PROJECT]', '--project [PROJECT]', "Xcode project path. Automatically look up if not provided.") do |project|
                options.project = project
            end

            opts.separator("")
            opts.separator("Help:")
            opts.on_tail('-h', '--help', 'Display this help') do
                puts opts
                exit
            end

        end

        opt_parser.parse!(args)
        options
    end # parse()
end

options = OptionParser.parse(ARGV)

# Lookup for Xcode project other than Pods
# @return [String] name of Xcode project or nil if not found
def lookup_project
    puts "Looking for Xcode project..."
    # list all .xcodeproj files except Pods
    projects_list = Dir.entries(".").select { |f| (f.end_with? ".xcodeproj") && (f != "Pods.xcodeproj") }
    projects_list.empty? ? nil : projects_list.first
end

# lookup if not specificed
options.project = lookup_project if !options.project
if !options.project then
    puts "Error".red.underline + ": No Xcode projects found in the working folder"
    exit 1
end

puts "Using project path: " + "#{options.project}".green

xcproj = Xcodeproj::Project.open(options.project)
xcproj.recreate_user_schemes
xcproj.save

Right, this is one of those cases where actual meaningful code is very small (just 4 lines), the rest is options parsing and error checking, but then it’s worth it in the end.

Finally run it

chmod +x share_schemes.rb
./share_schemes.rb -p "MyProject.xcodeproj"

P.S.

Related thread on StackOverflow.

Published: May 29 2014

blog comments powered by Disqus