Reputation: 441
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
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
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 Int
s, 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 T
s 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
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
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