Command line arguments with user defaults

You can use UserDefaults as a simple way to get the arguments passed to an app on launch without having to write any command line parsing. The basic capability is that you can pass an argument like -example some-string as a launch argument and this will be readable by defaults like this:

UserDefaults.standard.string(forKey: "example") //=> "some-string"

Supported types

UserDefaults supports a few types Array, Dictionary, Boolean, Data, Date, Number and String. It is possible to inject data and have them be understood in any of these types, the key is recognising that you need to use the same representation that plists use.


Arrays are heterogeneous and can be represented like this

// -example <array><string>A</string><integer>1</integer></array>
UserDefaults.standard.array(forKey: "example") //=> Optional([A, 1])


Any key value pair

// -example <dict><key>A</key><integer>1</integer><key>B</key><integer>2</integer></dict>
UserDefaults.standard.dictionary(forKey: "example") //=> Optional(["B": 2, "A": 1])


This can be represented by many variants. All of <true/>, 1 and true will count as true whereas <false/>, 0 and false will count as false.

// -example <true/>
UserDefaults.standard.bool(forKey: "example") //=> true


// -example <data>SGVsbG8sIHdvcmxkIQ==</data> "example")
    .flatMap { String(decoding: $0, as: UTF8.self) } //=> Optional("Hello, world!")


Date doesn’t have a convenience function but still returns an honest date when encoded correctly

// -example <date>2024-03-10T22:19:00Z</date>
UserDefaults.standard.object(forKey: "example") as? Date //=> Optional(2024-03-10 22:19:00 +0000)


For numbers you have the option to not wrap in any tags and hope for the best or to choose real or integer

// -example <real>1.23</real>
UserDefaults.standard.float(forKey: "example")   //=> 1.23
UserDefaults.standard.double(forKey: "example")  //=> 1.23
UserDefaults.standard.integer(forKey: "example") //=> 1

// -example <integer>1</integer>
UserDefaults.standard.float(forKey: "example")   //=> 1.0
UserDefaults.standard.double(forKey: "example")  //=> 1.0
UserDefaults.standard.integer(forKey: "example") //=> 1

Interestingly if you don’t provide a tag then integer(forKey:) doesn’t truncate it just returns 0

// -example 1.23
UserDefaults.standard.integer(forKey: "example") //=> 0


Strings can simply be passed directly unless you want to do anything more complex in which case you’d want to wrap in <string></string> tags.

Piecing things together

In some cases you might want to pass more complex data. One such example I came across was wanting to inject in a user profile that has multiple properties for UI testing. I could design the api so that the UI tests would pass multiple arguments but that would require validation and error handling in the app. It would be handy if I could just pass a JSON blob and then decode it inside the app. This is possible with two additional steps

Let’s imagine I have the following fictitious User type

struct User: Decodable {
    let name: String
    let token: String
    let isPaidMember: Bool

It might be handy in my UI tests to perform a login once at the beginning of all tests to get a token and then inject it into all the tests. I can also toggle the status of isPaidMember in each test. An appropriate argument that would work would look like this:

# {"name":"Paul","token":"some-token","isPaidMember":true}
-user <string>{&quot;name&quot;:&quot;Paul&quot;,&quot;token&quot;:&quot;some-token&quot;,&quot;isPaidMember&quot;:true}</string>

The corresponding code to parse this and fail silently in case of error would look like this

let user = UserDefaults.standard.string(forKey: "user")
    .flatMap { try? JSONDecoder().decode(User.self, from: Data($0.utf8)) }


UserDefaults can make launching your app and injecting data pretty simple. All your favourite primitive types are supported and once you get your head around using the plist markup you can start passing all kinds of data in just the right format for your needs.