Launch arguments - the mysteries

Marin Usalj at Swift Summit San Francisco, 2016


Transcript:

Marin: All right, thank you for coming everybody. This talk is going to be about launch arguments. And then it's “launch arguments - the mysteries” because a lot of these things are undocumented and obscured.

My name is Marin. This is me on the internet; You can find me on GitHub as @supermarin. My website is supermar.in. You might know me as the author of Alcatraz, xcpretty. For the past couple of years I've been working at Lyft. We are one of the first big apps that are 100% in Swift.

This talk will share some things that I learned while working there. So quick agenda. We'll go through some basics of process arguments, just to be clear what we're talking about. Then we'll cover some advanced use cases. And then I'll go over some gotchas to show the things that you should watch out for.

Right, so lets talk about process arguments for a second. There's a high chance that you have already been using some of them in system tools like LS or tools like Git. In this example of Git, Git would be the zero process argument, commit first, second, third, fourth. They're used by exec() family of functions. I think OS10 uses execve. They're used when you're launching binaries or scripts that start with a shebang.

In the C family of languages you would see that main with argc and argv, or if you're in Xcode you can use this nifty selector that ... got to give credit to Keith for finding this. True story.

Let's go to show some demos to see what we're working here. I'm showing the “Simplenote” open source app, there is no bias to choosing this app, it's mostly because it's on GitHub and if you want to play with this later you can try. I have not modified any source in this code. If you look, the app is all white teamed, it's in English, and without changing any source, you can basically change the locale to French and let's put direct team ... I'll close, relaunch ... it takes a second. All right. And the app is dark, you can see the menus are in french, and again this is all without changing any source, this is all by changing how the app is launched.

I'll show a couple more before I move on, one is called NSShowNonLocalizedStrings. And basically this one will make all the strings that are not local yell at you, they'll be all caps so if you're working with your app you can spot them easier. I took out the trash localizations out of the app, it's localized on github. If you don't localize, you see, it's all caps.

One more useful one, it's called NSDoubleLocalizedStrings, and this one will make every string you have in the app double. So this is used for some languages like German, that has huge words. To make sure that your labels are not falling apart. For example you see everything is now double, even “ok” says "ok ok".

I'll quote Mattt Thompson here: “There are a number of options that can be passed into a target’s scheme to enable useful debugging behavior, but like a fast food secret menu, they’re obscure and largely unknown”. There's way more of them, there's for Core Data, more for localiazation. They're made by Apple, but I'm not sure if they're documented anywhere. People would find them… you can find them on blog posts. They're usually very useful, and this talk is not going to show off all of them, we'll focus on how to use this yourself.

How does this work?

In most programming environments, you would process arguments yourself. For example in Bash, you can use getup or getopt or getopts, in Ruby there's a built in OptionParser, or a million different libraries. Python has argparse. Also from now on I'll be moving out of Xcode. I'll just show you the last example. If you came from the world of Objective-C, you might have seen this. It was probably pretty useless, I'm not sure if you did anything inside. If you did, probably it wasn't the best idea.

I'll just relaunch the SimpleNote app once more. What I'm doing here is just bringing this argument count and going through the arguments one by one. There's nine arguments, zeroth argument is the binary name, and it just listed all of them one by one.

Okay, so no more Xcode.

Why am I showing this? In Cocoa there is a little magic. Cocoa does some of this stuff for you. Arguments, they are starting with one dash, and then a key, space, value, are basically parsed for you. They are injected on top of NSUserDefaults, this doesn't mean you need to have something in NSUserDefaults to use them. This also means that anything that is stored in NSUserDefaults can be overwritten by launch arguments. In this simple diagram, let's say you have an app that has conference = NSSpain and speaker = Marin stored in defaults, and then you launch it with conference = SwiftSummit. Then you look up somewhere in the app, what is the NSUserDefaults objectForKey “speaker”, it will return “Marin”. But then if you look up conference, you will find it in launch arguments, it will not even look in NSUserDefaults. It will just return “SwiftSummit”.

You might be asking yourself "okay what do I do with this". One of the cool things you can do is you can change some of the app's behavior without recompiling it. For example at Lyft we are running a lot of checks each time somebody pushes the code in a pull request. There's various different deployments that we do, we run unit tests, integration tests, code lint, so people don't fight over white space. The one that I'll focus on is running end-to-end tests.

End-to-end tests at Lyft are spinning real devices and simulated with real servers and creating users, creating rides, signing up, changing credit cards, stuff like that. By real servers I didn't mean you work in production, you have a dev server. The first approach with this, very early days, we had all these devices getting one dev server. This was terrible because imagine you push your code and probably will spin ten simulators just for you. Because it's way faster to distribute… to parallelize tests. All these ten different cases are sitting in one server and then Anna pushes and John pushes and then you have thirty of them on one server. The problem here is we were having some failures that were not our fault. You do not want to have that.

The ideal goal is we launch a bunch of simulators and also bring up a bunch of servers, and each simulator talks to a different server. The only problem is once you compile the app, how do you tell the app to point to different servers?

The first approach we had, you can just use UIAutomation and accessibility. You launch the app, you bring up some secret server selector, you start typing, typing, typing, typing, and it takes a while to write the whole URI. To illustrate; You launch the app pretty fast, then you waste a lot of time to select the servers and then you run the test in seconds. We wanted to get from there to something like this. By reducing this time we are also increasing throughput, we can use less simulators, we can build more with what we have.

What can we do? We can recompile the app, let's say you push and we can recompile the app, ten different apps, each one pointing at a different server. Five minutes per compile, we just wasted fifty CI minutes. So we will not recompile.

We can also inject a file, say we compile once but we inject to different servers to each app that we run. This is almost a viable solution, but it would require adding some code that wouldn't be used in production at all. So we found we can use launch arguments without changing any code to the app. The only problem is our server environment is stored in the dictionary. So for example, the navigation app you can pass easily. Dash, navigation, space, “Waze", it works. How does one pass the dictionary to the command line.

Which brings us to the next topic; advanced usage.

Some Cocoa conventions would be key values, maybe you could use -servers.API, pass the string, -servers.analytics, doesn't work. Maybe we can use JSON. No.

Let’s do a quick quiz.

I hope you all should be familiar with a lot of this. Basically this is a simple Swift file that basically just reads a value from NSUserDefaults, isMember. If we have a value and if it's true, it will print a “member”, or else it will print a “non-member”, and it will also print out a type.

Question for you: which of the following inputs will print Member, Type: Bool?

Show hands for: 1. … A few.

Or: true?

What about: YES?

Well let's see. No, it's not 1. Got a string.

True? It's a different kind of string, but it doesn't work.

YES, it has to be YES, right?

No? Why not?

It's a string.

So I'll let you guess this one after I show you the other one. So let's get back to passing that dictionary. Basically what I'm going to do here is I'll run this file, sorry, I'll introduce the file. We're just setting a simple dictionary to NSUserDefaults, forKey: “servers”) and then we're just looping through each key value, printing it out. Very simple.

If you just run this file it will print out, analytics is pointing to, analytics.lyft.com. Api have become api.lyft.com. We want to override this, we want to point to staging. What we are going to do here is I'm going to just go on the command line and just going to show a bunch of xml, and it works!

If you're wondering "what", let's see this again.

There's actually something even better. You can basically show a bunch of ... what is this?

It's a next step NeXTSTEP plist, it's like {inaudible}. Works!

So can you guess how you pass a boolean? How you make it pass?

Plist? What do I type?

All right, good guess. I made it pass.

Which reminds me of these tweets.

Quick thing on NeXTSTEP plist, there is a reason why Apple switched to xml. Basically they don't store any NSValue objects, you cannot store dates, numbers, booleans, stuff like that. They only hold strings, so you cannot do things like this.

What I'm trying to say is that when they are launching an iOS app, your app, Mac app somebody else's app, the launcher is trying to parse xml from this way. It's trying to parse Plist. I'm not sure about the order of this, I didn't decompile. If it doesn't find any of them, it'll just use a string. To even just quickly prove this, we can break the plist parser, so we can go here and just trick it to start parsing the plist, and if we break it, it says CFPropertyListCreateFromXMLData, old-style plist parser: missing semicolon blah blah". If you don't believe me, try it with your app.

That brings us to the third section, which is: Gotchas. This one is important. Once you've already launched the app with arguments, there is no way you can override it. So if you write explicitly or remove a key from NSUserDefaults, it will still be there. So watch out in places in your app, where you are accessing NSUserDefaults. I can show you quickly in this third example, if we just launch this file, basically what it does is reads what is the conference, then sets NSSpain for conference, and then removes the conference, and prints each time. If you just launch the file, one app launch is nothing, after writing is NSSpain, after deleting, it's nil. As expected.

But if you launch this same file with a launch argument, you'll override everything. You see even after writing and resettingit's still respecting a launch argument.

UserDefaults is a key-value Store with String keys. It stores string, data, number, date, array and dictionary, and I've prepared you a folding table that I'll show you later. Probably should use helpers. How I tricked you in the first example is you mostly were using some of these helpers you had before. That's why you were reading strings as booleans or strings as numbers. These helpers are useful, if you want to be safer, use them. If you need to be more performance, maybe you want to be explicit and just do yourself. For example if you use -integerForKey: it says: “-integerForKey: is equivalent to -objectForKey, except that it converts the return value to an NSInteger. If the value is an NSNumber, the result of -integerValue will be returned. If the value is an NSString, it will be converted to NSInteger if possible. If the value is a boolean it will be converted to either 1 for YES or 0 for NO. If the value is absent, it'll be a zero. It does quite a few checks for it.

If you want to play with this and you want to use this in your app, this is how you pass the values through xml. If you want to pass dates, you just use the ISO 8601 formatted string. You can pass in this data as Base64 data. You can do this of course to the apps that you did not write, but I didn't tell you that.

I would like to say thanks to Lyft for supporting me in being here, Ida and Beren for inviting me to speak. I work for Lyft in San Francisco, we are hiring, if you are interested in talking with us come find me, and if there's time I'm happy to answer any questions.

objectForKey “speaker”, it will return “Marin”. But then if you look up conference, you will find it in launch arguments, it will not even look in NSUserDefaults. It will just return “SwiftSummit”.

You might be asking yourself "okay what do I do with this". One of the cool things you can do is you can change some of the app's behavior without recompiling it. For example at Lyft we are running a lot of checks each time somebody pushes the code in a pull request. There's various different deployments that we do, we run unit tests, integration tests, code lint, so people don't fight over white space. The one that I'll focus on is running end-to-end tests.

End-to-end tests at Lyft are spinning real devices and simulated with real servers and creating users, creating rides, signing up, changing credit cards, stuff like that. By real servers I didn't mean you work in production, you have a dev server. The first approach with this, very early days, we had all these devices getting one dev server. This was terrible because imagine you push your code and probably will spin ten simulators just for you. Because it's way faster to distribute… to parallelize tests. All these ten different cases are sitting in one server and then Anna pushes and John pushes and then you have thirty of them on one server. The problem here is we were having some failures that were not our fault. You do not want to have that.

The ideal goal is we launch a bunch of simulators and also bring up a bunch of servers, and each simulator talks to a different server. The only problem is once you compile the app, how do you tell the app to point to different servers?

The first approach we had, you can just use UIAutomation and accessibility. You launch the app, you bring up some secret server selector, you start typing, typing, typing, typing, and it takes a while to write the whole URI. To illustrate; You launch the app pretty fast, then you waste a lot of time to select the servers and then you run the test in seconds. We wanted to get from there to something like this. By reducing this time we are also increasing throughput, we can use less simulators, we can build more with what we have.

What can we do? We can recompile the app, let's say you push and we can recompile the app, ten different apps, each one pointing at a different server. Five minutes per compile, we just wasted fifty CI minutes. So we will not recompile.

We can also inject a file, say we compile once but we inject to different servers to each app that we run. This is almost a viable solution, but it would require adding some code that wouldn't be used in production at all. So we found we can use launch arguments without changing any code to the app. The only problem is our server environment is stored in the dictionary. So for example, the navigation app you can pass easily. Dash, navigation, space, “Waze", it works. How does one pass the dictionary to the command line.

Which brings us to the next topic; advanced usage.

Some Cocoa conventions would be key values, maybe you could use -servers.API, pass the string, -servers.analytics, doesn't work. Maybe we can use JSON. No.

Let’s do a quick quiz.

I hope you all should be familiar with a lot of this. Basically this is a simple Swift file that basically just reads a value from NSUserDefaults, isMember. If we have a value and if it's true, it will print a “member”, or else it will print a “non-member”, and it will also print out a type.

Question for you: which of the following inputs will print Member, Type: Bool?

Show hands for: 1. … A few.

Or: true?

What about: YES?

Well let's see. No, it's not 1. Got a string.

True? It's a different kind of string, but it doesn't work.

YES, it has to be YES, right?

No? Why not?

It's a string.

So I'll let you guess this one after I show you the other one. So let's get back to passing that dictionary. Basically what I'm going to do here is I'll run this file, sorry, I'll introduce the file. We're just setting a simple dictionary to NSUserDefaults, forKey: “servers”) and then we're just looping through each key value, printing it out. Very simple.

If you just run this file it will print out, analytics is pointing to, analytics.lyft.com. Api have become api.lyft.com. We want to override this, we want to point to staging. What we are going to do here is I'm going to just go on the command line and just going to show a bunch of xml, and it works!

If you're wondering "what", let's see this again.

There's actually something even better. You can basically show a bunch of ... what is this?

It's a next step NeXTSTEP plist, it's like {inaudible}. Works!

So can you guess how you pass a boolean? How you make it pass?

Plist? What do I type?

All right, good guess. I made it pass.

Which reminds me of these tweets.

Quick thing on NeXTSTEP plist, there is a reason why Apple switched to xml. Basically they don't store any NSValue objects, you cannot store dates, numbers, booleans, stuff like that. They only hold strings, so you cannot do things like this.

What I'm trying to say is that when they are launching an iOS app, your app, Mac app somebody else's app, the launcher is trying to parse xml from this way. It's trying to parse Plist. I'm not sure about the order of this, I didn't decompile. If it doesn't find any of them, it'll just use a string. To even just quickly prove this, we can break the plist parser, so we can go here and just trick it to start parsing the plist, and if we break it, it says CFPropertyListCreateFromXMLData, old-style plist parser: missing semicolon blah blah". If you don't believe me, try it with your app.

That brings us to the third section, which is: Gotchas. This one is important. Once you've already launched the app with arguments, there is no way you can override it. So if you write explicitly or remove a key from NSUserDefaults, it will still be there. So watch out in places in your app, where you are accessing NSUserDefaults. I can show you quickly in this third example, if we just launch this file, basically what it does is reads what is the conference, then sets NSSpain for conference, and then removes the conference, and prints each time. If you just launch the file, one app launch is nothing, after writing is NSSpain, after deleting, it's nil. As expected.

But if you launch this same file with a launch argument, you'll override everything. You see even after writing and resettingit's still respecting a launch argument.

UserDefaults is a key-value Store with String keys. It stores string, data, number, date, array and dictionary, and I've prepared you a folding table that I'll show you later. Probably should use helpers. How I tricked you in the first example is you mostly were using some of these helpers you had before. That's why you were reading strings as booleans or strings as numbers. These helpers are useful, if you want to be safer, use them. If you need to be more performance, maybe you want to be explicit and just do yourself. For example if you use -integerForKey: it says: “-integerForKey: is equivalent to -objectForKey, except that it converts the return value to an NSInteger. If the value is an NSNumber, the result of -integerValue will be returned. If the value is an NSString, it will be converted to NSInteger if possible. If the value is a boolean it will be converted to either 1 for YES or 0 for NO. If the value is absent, it'll be a zero. It does quite a few checks for it.

If you want to play with this and you want to use this in your app, this is how you pass the values through xml. If you want to pass dates, you just use the ISO 8601 formatted string. You can pass in this data as Base64 data. You can do this of course to the apps that you did not write, but I didn't tell you that.

I would like to say thanks to Lyft for supporting me in being here, Ida and Beren for inviting me to speak. I work for Lyft in San Francisco, we are hiring, if you are interested in talking with us come find me, and if there's time I'm happy to answer any questions.