Cómo incluir un argumento extra en una llamada a un closure en Swift para iOS

Depende de cómo se llame al closure y de la cantidad de shorthand/inferencia que se utilice.

Skip to the end for main answer to this, what follows first is a fairly deep dive into the subtle interplays of type inference, shorthand, closure expressions, trailing closures and a few others.

A closure is just a block of code which is usually defined in braces.

  1. print("hello") 

Si escribes lo anterior en un playground, da el error "Closure expression is unused". Esto nos dice dos cosas, primero, que efectivamente es un cierre, y segundo, que no se está usando para nada.

Esto es en realidad una gran pista, porque cómo y dónde expresamos los cierres está muy ligado a cómo y dónde los usamos.

Para lo anterior, normalmente podríamos verlo expresado como una función. A Function can be thought of as a special case closure that is given a name which we will call when we want to use it.

  1. func greet() { 
  2. print("hello") 
  3.  
  4. greet() 
  5. // prints "hello" 

So here, the braces define the block of code, while func assigns a name, parameter list and a return type to that block. Llamamos a la función usando su nombre y el bloque se ejecuta.

Ahora puede sorprenderte ver el tipo de retorno mencionado aquí porque no se especificó ningún tipo de retorno y la función tampoco termina con una declaración de retorno.

Esto es la abreviatura de Swift y la inferencia de tipos en acción. Ambos juegan un gran papel en Swift, y especialmente en los cierres. Both of these are great for the experienced programmer because they can write code much more concisely.

However, beginners typically learn a lot about a language by reading through other people’s code, and both shorthand and type inference refer to inferred code that isn’t actually there to be read! So when learning, it’s actually more useful to peel back the shorthand and inference and be more explicit about what is happening.

It’s a feature of Swift that blocks without a return value will implicitly return a Void. So the above function is actually shorthand for:

  1. func greet() -> Void { 
  2. print("hello") 
  3. return Void() 
  4.  
  5. greet() 
  6. // prints "hello" 

As a function, it has a return type of type Void, but as a block it has type of:

  1. () -> Void 

This means it takes no parameter and returns a Void. As a named function it also has a signature composed of its name and type, so:

  1. greet() -> Void 

Note, I’m continuing to use Void intentionally here, but you won't usually see it written like that. You will usually find it either omitted or written as (), so the block type is:

  1. () -> () 

However, I think that extra parens can sometimes impair readability, especially while learning, so I’ll stick with Void in these examples to help emphasise the context.

Now let’s introduce parameters into the mix:

  1. func greet(user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // fails with missing argument label error 

Here, by introducing the parameter, I have inadvertently fallen foul of shorthand again. The specification for a function parameter actually takes two names per parameter:

  1. func name(argumentLabel parameterName: paramType) 

The parameterName: paramType are the parameter’s name and Type. This is what I specified with:

  1. greet(user: String) 

Because I didn't specify an argument label, I was writing shorthand for using the same name for both the argument label and the parameter name, kind of like I’d actually written:

  1. greet(user user: String) 

So in order to use this function I will need to specify the user: argument label when calling greet.

  1. func greet(user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet(user: "Andy") 
  6. // prints "hello Andy" 

I could use a different name for the argument label

  1. func greet(username user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet(username: "Andy") 
  6. // prints "hello Andy" 

Or I could decide that the label is superfluous and eliminate it.

  1. func greet(_ user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

So even for simple functions shorthand and type inference plays a role in how you express and call the function.

With other closures, the syntax and execution call can vary much more significantly so it’s even more important to keep track of what is actually happening.

Let’s start simple and just replace the function with a regular closure expression assigned to a constant.

  1. let greet = {(_ user: String) -> Void in 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

You can see just how similar to a function this is, but even so, there are some extra important aspects to be aware of.

Most importantly, look how the parameter list and return type specification have migrated into the start of the block. To separate this spec from the main body we use the ‘in’ keyword.

Another difference is that although the parameter definition looks the same, we can no longer use argument labels for closures so the “_” isn’t needed here, in other words, we should write it like this:

  1. let greet = {(user: String) -> Void in 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

To pass additional parameters into the closure, we simply specify multiple parameters in the closure, and pass multiple arguments when we call for execution:

  1. let greet = {(user: String, isAM: Bool) -> Void in 
  2. print("good (isAM ? "morning" : "afternoon") (user)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

It’s also worth noting that type inference is also happening in this definition. The constant called greet is a reference to a closure which has a type of:

  1. (String, Bool) -> Void 
  2.  
  3. // aka (String, Bool) -> () 

Swift has inferred this type from the definition of the closure, so we don’t need to specify it, but in the spirit of unrolling all the shortcuts, lets add it in:

  1. let greet: (String, Bool) -> Void = {(user: String, isAM: Bool) -> Void in 
  2. print("good (isAM ? "morning" : "afternoon") (user)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

Type inference works the other way around too. Because we have specified the type of greet, Swift knows the parameters and return value of the closure.

This means these can be omitted from the closure, but in doing so the parameter names will be lost. The body still has access to the argument(s) but they are now anonymous and so are given the names $0 for the first, $1 for the second, and so on.

  1. let greet: (String, Bool) -> Void = { 
  2. print("good ($1 ? "morning" : "afternoon") ($0)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

Now obviously this would not be a good solution in this case because without the parameter names we have lost some code readability, but anonymous arguments do play an important role in exploiting shorthand, and in particular when we don’t call the closure ourselves.

To demonstrate, we’ll adapt an example from the Swift language guide:

  1. let names = ["Chris", "Alice", "Bob", "Arnold"] 
  2.  
  3. let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 
  4. return s1 > s2 
  5. }) 
  6.  
  7. print(reversedNames) 
  8. // prints ["Chris", "Bob", "Arnold", "Alice"] 

.sorted is an instance method that works on an array and returns a copy of the array in sorted order.

It can optionally take a parameter with the argument label ‘by:’ which can be used to pass a closure to determines how the array elements are to be sorted.

Rather than dive right in to what it does, instead lets first see the shorthand version

  1. let reversedNames = names.sorted(by: >) 
  2.  
  3. print(reversedNames) 
  4. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Through type inference, shorthand and something called operator methods, this:

  1. (by: { (s1: String, s2: String) -> Bool in return s1 > s2 }) 

becomes this:

  1. (by: >) 

Let’s break it down:

The last (only) expression ‘s1 > s2’ returns a boolean so the explicit return can be dropped:

  1. (by: { (s1: String, s2: String) -> Bool in s1 > s2 }) 

Type inference can infer the type as:

  1. (String, String) -> Bool 

so that can be dropped:

  1. (by: {s1, s2 in s1 > s2 }) 

anonymous arguments can be used:

  1. (by: { $0 > $1 }) 

Now if we wanted to stop there, there is a little syntactic sugar that can be applied when the closure is the last argument, it can be move outside the parens (and the argument label can be dropped), this is called trailing closure syntax.:

  1. () { $0 > $1 } 

This is how you will often see this type of closure used, but in our case there is a different feature we can use which is Operator Methods.

For Strings, the ‘>’ (greater than) operator is defined as a method whose type is also:

  1. (String, String) -> Bool 

So in this instance the ‘>’ method operator happens to match our closure so we can simply pass the ‘>’ operator, hence:

  1. let reversedNames = names.sorted(by: >) 
  2.  
  3. print(reversedNames) 
  4. // prints ["Chris", "Bob", "Arnold", "Alice"] 

One last aspect to consider is when we want the closure to have access to other values.

When we both created and called the closure ourself, we could simply pass it any values we chose as parameters/arguments. Pero cuando el cierre está siendo llamado por otro código, como el método .sorted, los argumentos son suministrados por el llamador, por lo que no't tenemos esa libertad para añadir argumentos adicionales. En su lugar, tenemos valores de captura.

Esto es en realidad muy simple y se hace mediante la referencia a un valor actualmente en el ámbito de aplicación a utilizar. Here’s a somewhat contrived but hopefully easy to follow example.

  1. let reversed = false 
  2. let sortedNames = names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 } 
  3. print(sortedNames) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 

Here, we have introduced a reversed constant and a reference to it is being captured in the closure and then used to decide whether or not to sort reversed.

Esto puede no ser inmediatamente obvio y estoy seguro de que algunos podrían discutir si cuenta como captura, pero en este contexto es discutible preocuparse por si el valor es capturado o simplemente está en el ámbito. The important part is the availability of the value for the closure.

To demonstrate ‘real’ capture, we need a slightly more complex example:

Rather than sort the name directly into a named copy, we instead create a ‘name sorter’ by enclosing the sort into its own closure:

  1. var reversed = false 
  2. let sorter = { names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Now, the sort is done within its own closure which can be called when we want the results.

Now we can show that we are really capturing the value of reversed by making it copy by value, which would mean when we change the value of reversed, the sorter would continue to use its own copy and not be affected by the change.

Here’s the first attempt:

  1. var reversed = false 
  2. let sorter = { names.sorted(by: ) { [reversed] in reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Here we told the inner closure (that gets passed to .sorted(by:)) to capture reversed as a copy, we did this by adding it to a capture list by putting it inside [] (square brackets). Esto también significa que tenemos que volver a introducir 'in' para separarlo del cuerpo.

Sin embargo, no tuvo el resultado deseado. El error es sutil.

Este cierre interno se crea cuando se llama a .sorted. Tal y como están las cosas, eso significa que invertido será cualquiera que sea su valor actual. En otras palabras, se está capturando dos veces, una en la primera llamada a .sorted cuando el valor es falso, y otra en la segunda llamada a .sorted, cuando el valor es verdadero. Hence, capturing in this closure has no impact.

Here’s how we actually capture a copy:

  1. var reversed = false 
  2. let sorter = { [reversed] in names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Alice", "Arnold", "Bob", "Chris"] 

Here we capture reversed in the outer closure, the one that gets assign to sorter.

This closure is created when it is assigned to sorter and so it captures a copy of reversed current value, which is false.

This copied value is now part of the sorter closure and that’s the value used whenever the closure is executed. No importa dónde cambiemos el valor del original invertido, el cierre siempre se ejecutará con su valor copiado establecido en falso.