One of the most common problems I run into during typical iOS development is needing to get access to my app’s directory. Unfortunately, apple has made this as difficult as they could by making it so that
- Each simulator has a non-deterministic directory
- Each application on that simulator has a non-deterministic directory
- Each time you install the app, it will get a new non-deterministic directory
Depending on the app you’re developing, you may be uninstalling and reinstalling multiple times a day, which means your app’s directory is going to change frequently. If you’re having to reset the simulators frequently, then it might be even worse.
The typical way people find their app directory is by throwing something like this into the app
1 |
NSLog(@"%@",[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]); |
However, this can be terribly inconvenient. I work in an environment where we don’t have this block of code as part of our git repo. I can toss this in, but I have to remember not to commit it. And finally, I usually don’t realize that I want this until after I have already built (we have an extremely slow build).
Luckily, there are some command line tools that can be chained together to solve these problems.
The goal of this script is to allow you to pick from a list of devices to open up an application directory for one of the apps installed on that simulator.
Note: I found out later that you can just add a --json
to the commands, and then you can have the output be in json, and then you could use a json deserialization library instead of writing all of the custom parsing. I will probably wait to rewrite this until it stops working. No sense in changing something just for the sake of change.
The steps we’re going to take in the script are
- Find a list of simulators and which apps are installed on each simulator
- filter out simulators that we don’t care about
- make a list of the bundle identifiers of each app installed on the simulators
- filter out simulators that don’t have any apps installed on them
- prompt for which simulator
- prompt for which bundle identifier
- open a finder window
As a reference, this class structure is one I used to keep data consistent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Device attr_accessor :name, :id, :os, :state, :bundles def initialize(os, name, id, state) @name = name.strip @id = id.strip.sub('(', '').sub(')', '') @os = os.strip @state = state.strip end def root_directory File.expand_path "~/Library/Developer/CoreSimulator/Devices/#{@id}/data" end end |
Getting the list of simulators is simple. its just an xcrun command and some parsing (as mentioned, this could have been done easier but i didn’t know about the json flag)
I also put in some code to filter out devices based on key word matching (ie, iPhone, Air, etc) But the overwhelming majority of that code is just for parsing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
## #get a list of the simulators def simulator_list(matching_words) device_output = `xcrun simctl list devices` current_os = "" devices = [] device_output.lines.drop(1).each do |line| os = line[/--(.*?)--/m,1] if os.nil? #found a device stuff = line.strip.reverse state = stuff.strip.slice(0..(stuff.index('('))) stuff.sub!(state, '') state.reverse! id = stuff.strip.slice(0..(stuff.index('('))) stuff.sub!(id, '') id.reverse! name = stuff.reverse if matching_words.all? { |word| name.include?(word) } device = Device.new(current_os, name, id, state) devices.push device end else current_os = os end end devices end |
I use it, and filter out devices that I’m not interested in like this
1 2 3 4 5 6 7 8 |
#find list of simulators and what apps are installed matching_simulators = simulator_list matching_words #exclude watches because i don't care about them matching_simulators = matching_simulators.select { |sim| !sim.os.include?('watchOS') } #exclude devices that have never launched matching_simulators = matching_simulators.select { |sim| File.directory?(sim.root_directory + '/Containers') } |
In my case, I don’t care about any watchOS devices because we aren’t doing apple watch development. The other filtering I have later really makes this quite useless, but it just speeds things up a little bit if you can strip down the list of devices before doing lots of i/o
After that, I populated a list of apps
1 2 3 4 5 6 7 8 9 10 11 |
def find_apps(device) search_string = device.root_directory + '/Containers/Bundle/Application/**/*.app/Info.plist' plists = Dir[search_string] bundles = [] plists.each do |file| tmp = CFPropertyList::List.new(:file => file) data = CFPropertyList.native_types(tmp.value) bundles.push data['CFBundleIdentifier'] end device.bundles = bundles end |
The only particularly interesting thing here is that i do a wildcard search of al application info.plist files and parse them for their bundle identifiers. You could also parse them for the app name to display instead of the bundle identifier. However if you release the same code in different regions under the same name (using different bundle identifiers to release to the different regions at different times of the day) then you might have duplicates, making the bundle identifiers more convenient.
The key here is that if you have the bundle identifier, then you can run an xcrun command to just get the full path to that app’s data directory. You can avoid the whole mess of non-deterministic paths
1 |
xcrun simctl get_app_container #{device.id} #{bundle} data |
Once you have the device objects and their bundle identifiers, it really is quite easy to throw together some simple i/o prompting
The final script is attached, but I don’t think it needs to be gone into further detail.