'Big configuration object as a parameter in Swift?

In JavaScript/TypeScript, we have this pattern that is often used in libraries where you have one optional parameter as the last function argument which defaults to some default configuration.

It looks like this:

const smth = someLibrary("requiredParameter", {
  someOptionalProperty: true,
  // ...other options available, but not required to be provided here.
});

On the library side it would look something like this:

export const someLibrary = (requiredParameter: string, options?: Options) => {
  const opts = { ...defaultOptions, ...options };

  // Here `opts` will have the default properties merged with the ones that you provided.
}

Now, in Swift, from what my 2-3 year experience has told me, if you want to do something similar you have to make a struct with all the configuration parameters and then mutate the defaults after instantiating the struct. This would look something like the following:

struct Options {
  var option1: Double = 2.0
  var option2: Int = 1
  // ...etc
}

public func someLibrary(_ requiredParameter: String, options: Options?) {
  // ...
}

let opts = Options()
opts.someParameter = "Overriding the values here"

let result = someLibrary("requiredParameter", options: )

The problem

There are a couple of problems with the Swift implementation that JavaScript does very well:

  1. Default options override, but just the fields that were changed - I haven't been able to replicate this in swift with a solution that is at least close in elegance.
  2. Partial override of the options - the approach in Swift that I was using is to have an initializer for the struct with all optional parameters, but the initializers become huge for big configurations.

The question

Now the question is - how close can I get to the JavaScript version using Swift?

Maybe there's a pattern or a function that I'm missing that would do that for me?

The goal for me is to not have huge boilerplate to make my library customisable.



Solution 1:[1]

Here a sample example :

struct Options {
    // option values are saved in private dictionary of Any
    private var options = [String:Any]()
    
    init(withOptions options: [String:Any]) {
        self.options = options
    }
    
    // Get int option
    subscript(intOption: String) -> Int {
        return options[intOption] as? Int ?? 0
    }

    // Get string option
    subscript(stringOption: String) -> String {
        return options[stringOption] as? String ?? ""
    }

    // Get float option
    subscript(floatOption: String) -> Float {
        return options[floatOption] as? Float ?? 0.0
    }
    
    // generic option but return an optional
    subscript<T>(withName: String) -> T? {
        return options[withName] as? T
    }
}

let options: Options = Options(withOptions: ["option1": 1,
                                             "option2": "string 2",
                                             "option3": Float(3.0), // set type if you want correct value
                                             "option4": 4.0])

let o1: Int = options["option1"]
let o2: String = options["option2"]
let o3: Float = options["option3"]
let o4: Double? = options["option4"]

This is a base that may help you, Here the subscript are an nice way go get values.

EDIT : added non typed access EDIT 2 : added Float for type accuracy

Solution 2:[2]

You said:

you have to make a struct with all the configuration parameters and then mutate the defaults

No, you can also set the overrides when you initialize the Options, e.g.,

struct Options {
    var option1: Double = 2.0
    var option2: Int = 1
    var option3: String = "foo"
}

func someLibrary(_ requiredParameter: String, options: Options = Options()) {
    // ...
}

No hairy “boilerplate hell” required.

So, yes, you can initialize it as a separate object and pass it as a parameter:

var options = Options()
options.option2 = 42

someLibrary("requiredParameter", options: options)

But you can also do it inline if you only have one or two overrides:

someLibrary("requiredParameter", options: Options(option2: 42))

or

someLibrary("requiredParameter", options: .init(option2: 42))

Theoretically, moving the defaults into the initializer is a tad more efficient:

struct Options {
    var option1: Double
    var option2: Int
    var option3: String

    init(
        option1: Double = 2.0,
        option2: Int = 1,
        option3: String = "foo"
    ) {
        self.option1 = option1
        self.option2 = option2
        self.option3 = option3
    }
}

I know you have dismissed this option as “boilerplate hell”, but sometimes it is the right approach (especially if any of these properties are computationally expensive or if you needed it to be a reference type, for example).

FWIW, Xcode also has “Editor” » “Refactor” » “Generate Memberwise Initializer” to simplify the generation of this initializer. Or, Xcode’s “multiple cursor” support (see Paul Hudson’s demonstration on YouTube) makes it pretty easy, too.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2