lispquestions
lispquestions

Reputation: 441

Closures in Swift: what are they? how do they work?

I'm working through a book on Swift basics and the section on closures made little sense to me.

This is the example that they provided:

To sort an array so that small numbers go before large numbers, you can provide a closure that describes how to do the sort, like this:

var numbers = [2,1,56,32,120,13]
// Sort so that small numbers go before large numbers

var numbersSorted = numbers.sort({
    (n1: Int, n2: Int) -> Bool in return n2 > n1
})
// = [1, 2, 13, 32, 56, 120]

I don't understand what the purpose of the closure is in this case -- wouldn't numbers.sort() return the same value even without the closure? Also, I'm not sure what the closure is actually doing stepwise here.

More broadly, I'm not certain from this example when you might use closures in Swift. Spiritually, they seem close to reduce functions, but I'm otherwise not sure when you would use them.

Edit: It also isn't helping that this code sample isn't working in the Swift playground that I'm using (v. 12.0). It's complaining that the call to sort is Missing argument label 'by:' in call. Maybe there's a better example of closures out there.

Upvotes: 1

Views: 1390

Answers (4)

roadRunner
roadRunner

Reputation: 201

I get confused when I used sort function first time like you because it was seem to me diffucult to understand passing data with this sort function specifically. You have basically 3 opportunities passing the data : notifications,protocols and closures. First of all think about this and please simply build a simple project(add button, label etc.) and try rather than playground :

class AVC: UIViewController{  
// some codes

@IBAction func firstButtonClick(_ sender: Any) { 
    let sb = UIStoryboard(name: "Main", bundle: nil)
    if let destVC = sb.instantiateViewController(withIdentifier: "BVC") as? BVC {
        destVC.successListener = { hello in
           
            let sb = UIStoryboard(name: "Main", bundle: nil)
            if let destVC = sb.instantiateViewController(withIdentifier: "CVC") as? CVC {
                destVC.helloStr = hello
                self.navigationController?.pushViewController(destVC, animated: true)
            }
        }
        self.present(destVC, animated: false, completion: nil)
        
    }
}

On the other hand lets see the destVC(Destination View Controller)

class BVC: UIViewController{

typealias SuccessListener = (String) -> ()
var successListener: SuccessListener?

//some codes
 @IBAction func secondButtonClick(_ sender: Any) {

    self.dismiss(animated: false, completion: {
          
          let str = "Hello World"
          self.successListener?(str)
    })
}

So When you trigger the second button you should consider two closure: First one is inside the dismiss function and its completion. When you call dismiss function it says hey you do it inside the completion as well. Because it is a completion like () -> () means take no parameter and returns nothing. Second one the success listener closure created by you and it has difference (String) -> () takes string as a parameter and returns nothing. Now tim go back to my example You can pass any String as a parameter to success listener in my case it is "str". It is ready to cook.

destVC.successListener = { hello in

This part no matter which you write hello hola ciao it will pass the str which is "Hello World"

Hope it will be helpful

Upvotes: 1

Sweeper
Sweeper

Reputation: 270780

In order to understand the purpose of the closure parameter, it might help to imagine that you are the person implementing Array.sort. You might start by write something like this (note: this is not how Array.sort is really implemented):

struct Array<T> {
    ...

    mutating func sort() { // bubble sort
        guard self.count > 1 else {
            return
        }
        
        for i in 0..<self.count {
            for j in 0..<(self.count - 1) - i {
                if (self[j + 1] < self[j]) { // compare the elements
                  self.swapAt(j, j + 1)
                }
            }
        }
    }

    ...
}

Then you realise the above code has an error. In the line where you are comparing elements, you realise that < can't be applied to any random T. T has to conform to Comparable before you can use < to compare them. So you think, okay, I'll make this sort method available only if T conforms to Comparable. And this is exactly why numbers.sort(), without the closure, works, as numbers is an array of Ints, and Int conforms to Comparable.

Then you thought, lots of other types aren't Comparable, but it would be nice to have a sort method on them too. People can just "tell me" how they want them to be compared. Also, for Comparable types, some users might not like to compare them with <, but with > instead. I want them to "tell me" this too.

So, you replaced the > with a function call:

if (shouldComeBefore(self[j + 1], self[j])) { // compare the elements

And added a parameter to sort:

mutating func sort(by shouldComeBefore: (T, T) -> Bool) {

The parameter is a function that takes two Ts as parameter, and returns a Bool. This return value indicates whether the first T should come after the second T in the sorted array.

Now let's try to call sort:

func descendingOrder(n1: Int, n2: Int) -> Bool {
    return n1 > n2
}

numbers.sort(by: descendingOrder)

Now you should see that by doing numbers.sort(by: descendingOrder), we are "telling" sort to use > to compare, rather than <.

It is quite a lot of trouble to write a function each time we call this sort, and that's why we have the closure syntax. The same thing can be written as:

numbers.sort(by: { (n1: Int, n2: Int) -> Bool in return n1 > n2 })

It is quite clear how the function descendingOrder is translated into the above closure. The closure can be further simplified to:

numbers.sort { $0 > n1 }

For more info on how to simplify closure syntax, see Closures in the Swift guide.

Upvotes: 1

Andrew
Andrew

Reputation: 28539

In really simple terms a closure is a function that can be passed around.

With regard to using sort, you may choose to sort on different properties of the object so you may wish to create your own custom sort function/closure.

Note there is a difference between sort and sorted in Swift. sort mutates the original array, sorted creates a copy of the array. So in your example numbersSorted won't have a value and you should use sorted if you want it to have a value.

let numbers = [2, 5, 3, 2, 1, 0, -8, 12]

let numbersSorted = numbers.sorted { first, second -> Bool in
    return first < second
}

print(numbers)              // [2, 5, 3, 2, 1, 0, -8, 12]
print(numbersSorted)        // [-8, 0, 1, 2, 2, 3, 5, 12]

However I don't need to write the above code like that. I could swap the closure for a function.

let numbers = [2, 5, 3, 2, 1, 0, -8, 12]

func ascending(first: Int, second: Int) -> Bool {
    first < second
}

let numbersSorted = numbers.sorted(by: ascending)

print(numbers)              // [2, 5, 3, 2, 1, 0, -8, 12]
print(numbersSorted)        // [-8, 0, 1, 2, 2, 3, 5, 12]

Note that the function ascending has the same signiture as the closure that we were using before.

Alternatively I can write a closure as a variable that can then be passed around

let numbers = [2, 5, 3, 2, 1, 0, -8, 12]

let ascending = { (first: Int, second: Int) in 
    return first < second
}

let numbersSorted = numbers.sorted(by: ascending)

print(numbers)              // [2, 5, 3, 2, 1, 0, -8, 12]
print(numbersSorted)        // [-8, 0, 1, 2, 2, 3, 5, 12]

The most important thing is that the function's and the closure's signature match. That way they can be swapped.

Personally I prefer going for the second option as it makes the code more readable and it can look cleaner.

There is another term that you have to contend with when it comes to closures, and that is @escaping. What does that mean?

An @escaping closure outlives the function that it was passed to. Basically it is saying that this code should be executed in the future after the function has been called.

This is quite often seen when networking calls as they can take a period of time to complete. The function will have executed but you haven't received the response, once the response has been returned then it executes the completion block (our closure) and we can see the updates.

Here are some good articles that you can follow up with

https://learnappmaking.com/closures-swift-how-to/

https://www.hackingwithswift.com/example-code/language/what-is-a-closure

https://www.swiftbysundell.com/basics/closures/

https://www.hackingwithswift.com/example-code/language/what-is-an-escaping-closure

Upvotes: 5

Rob
Rob

Reputation: 2164

A network request without closure, let's take an example of getting the users from the server:

func getUsers() -> Users { //Users is coddle
 
 guard let url = URL(string:"https://your-url.com") else {
    print("Invalid Url")
    return
 }

 let request = URLRequest(url: url)
 request.httpMethod = "GET"
 request.setValue("application/json", forHTTPHeaderField: "Content-Type")

 URLSession.shared.dataTask(with: request) { [weak self](data, response, error) in 

  if let error = error { print( error) }

    if let data = data { 

      //Get your JSON from data here. I user decodable so, I will return 

     guard let users = try? JSONDecoder().decode(Users.self) else { return }
     print("These are my users", users)
      return users
     }
   }

Now, in the above example you are requesting the server to give you the user records and it is a GET request. When you will hit this request you expect to get the user records, but it will fallthrough. Like if you write it in @IBAction function then you will see this:

func btnTap(_ sender: UIButton) { 
  let users = getUsers() 
  print("Here are my users")
}

Now, you will see that once the button is tapped it will call the function, but it will not wait for the server to return anything as there is no closure, the users will always be empty. It will print "Here are my users" first and then after sometime you will see it will print "These are my users (All Users)".

So, to solve this we need to add a call back or a closure. Ironically, URLSession.shared.dataTask(with: request) { [weak self](data, response, error) in is a closure itself.

Now, the same function with closure:

 func getUsersWithCallback(completion: @escaping(_ users: [Users], _ error: Error?) -> Void) -> { //Users is codable

 guard let url = URL(string:"https://your-url.com") else {
    print("Invalid Url")
    completion(nil, nil) //Handle this
    return
 }

 let request = URLRequest(url: url)
 request.httpMethod = "GET"
 request.setValue("application/json", forHTTPHeaderField: "Content-Type")

 URLSession.shared.dataTask(with: request) { [weak self](data, response, error) in 
  if let error = error { 
  print( error) 
 completion(nil, error) //Now, this is a callback, once you receive an error from the server it will pass the error here and this function will return a value only once the completion is achieved or we pass a call back.
}

if let data = data { 

  //Get your JSON from data here. I user decodable so, I will return 

 guard let users = try? JSONDecoder().decode([Users].self) else { return }
 print("These are my users", users)
  completion(users, nil) //Now this will return the users
 }
 }

Now, this is an example with closure above, by adding closure we are telling the function to wait. There is something coming from the server and we are dependent on the server now to return the response, so we wait for server to return a response now. Previously, we were not waiting for the server to response.

Now, our @IBAction will get the value:

func btnTap(_ sender: UIButton) { 
  let users = getUsersWithCallback( [weak self] (users, error) in 
    print("Users", users)
  }
  print("Here are my users")
}

Now, you will get the values of all the users as they are now received from the server and we have waited enough to get the call back. So, that is closure. This same thing is done with sort, to sort the above array it has to compare a value with all the remaining values, once that is completed it gives you the array.

Upvotes: 1

Related Questions