Back to blog

The Ultimate Guide to Closures in Swift


Aasif Khan
By Aasif Khan | Last Updated on November 18th, 2023 12:00 pm | 6-min read

This tutorial dives into Swift closures. Closures are blocks of code that you can pass around in your code, as if you assign a function to a variable. Mastering closures is a crucial aspect of learning iOS development.

If you had a tough time understanding optionals, then you’ll probably find the prospect of mastering Swift’s closures even more terrifying! Don’t worry though, they are more harmless than they look. And closures are useful, too!

Here’s what you’ll learn:

  • How closures work and how you can use them
  • The syntax for declaring and calling closures
  • How to compose and deconstruct a closure’s type
  • What “closing over” means, and how to use a capture list
  • How to use a closure effectively as a completion handler

You’ll have to forgive me for the cheesy tropical island image at the top of this tutorial. For starters, it’s really hard to find a visual representation of an abstract concept like a closure. On top of that, finally “getting” closures is kinda like arriving at a tropical destination. And by the way, you’ll definitely need a happy place to endure the journey towards the mastery of closures… 😉

How A Closure Works In Swift

Closures are self-contained blocks of functionality that can be passed around and used in your code.

Said differently, a closure is a block of code that you can assign to a variable. You can then pass it around in your code, for instance to another function. That function then calls the closure and executes its code, as if the closure is an ordinary function.

As you know, variables store information in your Swift code, and functions can execute tasks. With closures, you put a function’s code in a variable, pass it around, and execute its code somewhere else.

Let’s look at an analogy:

  • Bob gives Alice an instruction, and tells her: “Wave your hands!” Alice hears the instruction, and waves her hands. The hand-waving is a function, that Bob has called directly.
  • Alice writes her age on a piece of paper, and gives it to Bob. The piece of paper is a variable. Alice used the piece of paper to store some data.
  • Bob writes “Wave your hands!” on a piece of paper and gives it to Alice. Alice reads the instruction on the piece of paper, and waves her hands. The instruction, as passed on the piece of paper, is a closure.
  • Later on, we can metaphorically send this piece of paper through the mail, or tell Alice to read it after she’s finished another task… That’s one of the benefits of using closures!

Makes sense, right? Let’s write a closure in Swift!

let birthday = {
print(“Happy birthday!”)
}

birthday()

Here’s what happens in that code:

  • On the first line, you’re defining a closure and assigning it to birthday. The closure is the stuff between the squiggly brackets { and }. See how it’s similar to a function declaration? Note that the closure is assigned to birthday with the assignment operator =.
  • On the last line, the closure is called. It’s executed by calling birthday(), the name of the constant birthday with parentheses (). This is similar to calling a function.
  • When you run the code, the closure is called with birthday(), which will execute the closure, and print out Happy birthday! with the print() function.
  • At this point, the type of birthday, and the type of the closure, is () -> (). More on closure types later.

Next, let’s add a parameter to the closure. Parameters are input values for functions and closures. Just like functions, closures can have parameters.

let birthday:(String) -> () = { name in
print(“Happy birthday, \(name)!”)
}

birthday(“Arthur”)

Alright, that’s getting more complicated. Let’s deconstruct that code bit by bit. Here’s what happens:

  • Just like before, we’re declaring the closure on the first line, then assign it to the constant birthday, and call the closure on the last line.
  • The closure now has one parameter of type String. This parameter is declared as part of the closure type (String) -> ().
  • You can then use the parameter name within the closure. When calling the closure, you provide a value for the parameter.

What do you make of that? In essence, there are three things that matter here:

  • The closure type (String) -> ()
  • The closure expression { name in }
  • The closure call birthday()

The parameters of a closure aren’t named, unlike Swift functions. When you declare a closure you can specify the types of parameters it has, such as String in the above example, but you don’t specify a parameter name – only the type.

In the closure expression, the { name in part, you assign a local variable name to the first parameter of the closure. This gives the parameter a name within the closure. You could have named it anything you wanted.

In fact, you could have left out a variable name, and used the $0 shorthand! Like this:

let birthday:(String) -> () = {
print(“Happy birthday, \($0)!”)
}

In the above code, the closure birthday has one parameter. Inside the closure, the shorthand $0 is used to reference the value of that first parameter.

When you call a closure, you don’t include a name for any parameters it has. This is different from a function, which usually has named parameters (called argument labels). Here’s how that works for closures:

birthday(“Arthur”)
// Output: Happy birthday, Arthur!

See how that works? We’re calling the closure birthday(_:) and provide one parameter “Arthur” of type String. This parameter doesn’t have a name, just a value. As we’ve discussed before, the value gets a name within the closure.

Here’s what you’ve learned so far:

  • A closure is a block of code that you can pass around in your code
  • Closures can have zero, one or more parameters
  • Every closure has a type, including any closure parameters

Let’s take a closer look at the type of a closure, in the next section.

Swift’s Closure Types Explained

Every closure has a type, just like any other variable, constant, property, etcetera.

Conceptually, there’s no difference between a variable that’s declared with type Int and a variable declared as type (Int) -> (). The former is has type integer, and the latter is a closure type. The types themselves are different, but they both are types.

You declare a closure with one parameter like this:

let birthday:(String) -> () = { (name:String) -> () in

}

That looks complicated, right? Here’s how it works:

  • The stuff between the squiggly brackets { and } is the closure expression. The closure assigned to the constant birthday with the let keyword.
  • The first (String) -> () is the closure type. This closure has one parameter of type String and it returns (), which means “nothing”.
  • The second (name:String) -> () is the same closure type, except that it names the first parameter of the closure as name. It’s part of the closure expression.
  • The in keyword separates the closure parameters and the closure body. Before in comes the closure type, and after in you write the code of the closure.

Let’s deconstruct closure types to better understand how closures work. In it’s most essential form, this is the syntax for a closure:

let closureName:(parameter types) -> return type = { (parameter name:parameter type) -> return type in

}

What’s going on? First off, we’ve got 3 important parts here:

  1. The name of the closure, the constant that the closure is assigned to, which is this syntax: let closureName
  2. The closure type syntax, which is (parameter types) -> return type
  3. The closure expression syntax, which is { (parameter name:parameter type) -> return type in }

The constant that we assign the closure to is simple. From now on, we can pass that closure around using its constant (or variable) name.

The syntax for the closure type consists of parameter types and a return type. Just like a function, a closure can have parameters and return a value. The syntax to declare this type consists of types wrapped in parentheses, followed by a single arrow, followed by another type. A few examples:

  • (Int, Int) -> Double, so 2 integer parameters, and returns a Double value
  • () -> Int, no parameters, returns an integer
  • (String) -> String, takes a string, returns a string

OK, let’s move on. What about the closure expression? Here it is again:

{ (parameter name:parameter type) -> return type in

}

If you look closely, you’ll see that the closure type is repeated here. We’ve started with an opening squiggly bracket {, then declare the closure type again, followed by in, followed by some Swift code, followed by a closing squiggly bracket }.

The closure type is declared differently here, though! It now includes names for the parameters. Let’s look at a few examples; the same as before:

  • (Int, Int) -> Double becomes { (width: Int, height: Int) -> Double in
  • () -> Int becomes { () -> Int in (nothing changes b/c no parameters)
  • (String) -> String becomes { (text: String) -> String in

You can name these parameters anything you want, but you will need to give them names. These constants can be used “locally” inside the closure, just like parameters within a function.

What about in? It’s easiest to think of “in” as in normal English language. So, we’ve got a closure, and we say: “We’ve got parameters X, Y, Z in this block of code; which is the closure.”

The above example uses the complete closure syntax. In the next section, we’ll discuss which parts of that syntax can be “inferred” and left out.

A closure with no parameters and no return type, has the following closure type:

() -> ()

The above expression consists of two empty tuples () and a single arrow ->. The first tuple is the input for the closure, and the second tuple is the output for the closure.

A tuple is an ordered list of items, like (a, b, c). It’s comma-separated and wrapped in parentheses. An example in Swift: let flight = (airport: “LAX”, airplane: 747). Read more about tuples here: Tuples In Swift Explained

You can also write Void for the closure’s return type, like this:

() -> Void

In Swift, Void means “nothing”. When your function returns Void, it does not return anything. Not even nil or an empty string! As described in the Apple Developer Documentation, Void is an alias of an empty tuple (). When a function or closure doesn’t return anything, its return type is Void.

Let’s look at one last example. The following function has two parameters, and returns a value:

let greeting:(String, String) -> String = { (time:String, name:String) -> String in
return “Good \(time), \(name)!”
}

let text = greeting(“morning”, “Arthur”)
print(text)

The closure greeting has two parameters, both of type String. The closure returns a value of type String. The type of greeting is defined explicitly, and so are the closure parameters in the closure expression itself.

When the closure is called, it is provided two arguments of type String, and its return value is assigned to text, and then printed out.

See how there are two parameters of type String, and a return type of type String? The closure type (String, String) -> String defines the input and output for the closure, and the expression (time:String, name:String) gives the input parameters local variable names.

As you’ll find out in the next chapter, you can write the exact same closure expression also like this:

let greeting:(String, String) -> String = { “Good \($0), \($1)!” }

let text = greeting(“morning”, “Arthur”)
print(text)

Let’s summarize:

  • Every closure has a type, that you define in the closure expression
  • The most basic closure type is () -> Void, with no input parameters and no return value
  • You can explicitly define a closure type when declaring a variable or constant, such as let greeting:(String) -> Void
  • The closure expression repeats the closure type and gives every closure parameters a name, like { (name:String) -> Void in

Awesome! Let’s move on.

Can you use optionals with closures? Yes! Any closure parameter type or return type can be optional, like this: (String?, Int) -> Int?. You can also make the closure itself optional, which means that the variable, constant or property that references the closure can be nil. You do this by wrapping the entire closure type declaration in parentheses, followed by a ?. Like this: let birthday:((String) -> ())? = . The constant birthday can now be nil or contain a closure.

Closures And Type Inference

Swift has a super useful feature called type inference. It works like this: When you don’t explicitly specify the type of a variable, Swift can figure out on its own what the type of that variable is. It does so based on the context of your code.

Here’s an example:

let age = 104

What’s the type of age? Swift will infer the type of age based on the context of the above code. That 104 is a literal value for an integer number, so the constant age has the type Int. Swift figures this out on its own, without your needing to provide an explicit type. Neat!

Important: Swift is a strong-typed programming language. That means that every value has a type, even if it is inferred! Never mistake type inference for “this value has no type”. A value always has a type, you just don’t declare it explicitly. Type inference can lead to confusion. Make sure you always know the types of your variables!

Type inference and closures go hand-in-hand. As a result, you can leave out many parts of a closure expression. Swift will (usually) infer those parts on its own.

Let’s look at an example:

let names = [“Zaphod”, “Slartibartfast”, “Trillian”, “Ford”, “Arthur”, “Marvin”]
let sortedNames = names.sorted(by: <) print(sortedNames) In the above code you’re creating an array with names, then sorting them alphabetically by calling the function sorted(by:), then assigning the result to sortedNames, and then printing out the array of sorted names. The key part of this example is names.sorted(by: <). That first parameter by: takes a closure. You can provide a closure that’s used to sort the array. We’re providing just <… Now… get a load of this. First, when we expand < it becomes this: names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 < s2 }) Then, that’s the same as… names.sorted(by: { s1, s2 in return s1 < s2 } ) And it’s also exactly the same as… names.sorted(by: { s1, s2 in s1 < s2 } ) And you can also write that like… names.sorted(by: { $0 < $1 } ) With trailing closure syntax, you can even write it as: names.sorted { $0 < $1 } And finally, you can also just use the < operator as a function: names.sorted(by: <) Neat, right? The functionality of these different syntaxes is exactly the same! All thanks to type inference and closure expressions. Here’s the gist of every closure expression above, starting with the most expanded one:

  1. The first example uses the complete closure syntax, including two parameter names s1 and s2 of type String, and a return type of Bool. It also uses the in and return keywords.
  2. The second example omits the closure parameter types, because they can be inferred from context. Since we’re sorting an array of strings, those two parameters are inferred as String. (When omitting the types, you can also omit the parentheses. This s1, s2 in type of syntax is common.)
  3. The third example omits return, because the closure is only one line of code. When that’s the case, Swift figures out that you want to return a value, even if you don’t include return.
  4. The fourth example uses the $0 and $1 shorthands to reference the first and second parameters of the closure. The parameters are there, they just don’t have names! Again, with inferred String types.
  5. The fifth example uses trailing closure syntax. When a closure is the last (or only) parameter of a function, you can write the closure outside the function’s parentheses.
  6. And the last example uses the smaller-than operator < as the closure. In Swift, operators are top-level functions. Its type is (lhs: (), rhs: ()) -> Bool, and that fits the type of sorted(by:) perfectly!
  7. It’s hopefully now that you’re starting to see the power of closures, their expressivity, and how the Swift programming language enables you to write elegant code. Closures are also important for SwiftUI’s syntax.

It helps to practice Swift coding and working with closures as often as you can. Practice makes perfect! Don’t wait to learn about closures until you’ve ran into trouble. Set aside some of your time to discover and grasp new programming concepts and best practices.

Personally, I like the sorted { $0 < $1 } expression most. Remember that it’s not always smartest to choose the most concise programming approach. In fact, it helps to be more descriptive than clever! By the way, closures can get crazy really quickly. Check this out: let squared = { $0 * $0 }(12) print(squared) The closure { $0 * $0 } is immediately called with (), right after defining it, effectively assigning the result of 12 * 12 to squared. Executing a closure directly after it is defined is the basis for lazy computed properties. So, to summarize:

  • Swift uses type inference to figure out the type of a variable or constant when it isn’t explicitly provided
  • Closures and type inference go well together, so you can leave out parts of a closure expression to improve readability – but not too much
  • Closure expressions can get crazy quickly, so it helps to practice using closures and play around with them

Trailing closure syntax is super helpful. Here’s how it works: When a closure is the last (or only) parameter of a function, you can write the closure outside the function’s parentheses, and omit that parameter’s label. So, the code sorted(by: { $0 < $1 } ) becomes sorted { $0 < $1 }. See how the label and parentheses have changed?

Closures & Capturing In Swift

Seeing a closure as a function you can assign to a variable doesn’t do the concept of closures the justice it deserves. When I explained closures like that, I left out an important part: capturing.

The name “closure” comes from “enclosing”, and when we stretch it, it comes from the functional programming concept of “closing over”. In Swift, a closure captures variables and constants from its surrounding scope.

The words “closing over”, “capturing” and “enclosing” all mean the same thing here.

Every variable, function and closure in Swift has a scope. Scope determines where you can access a particular variable, function or closure. If a variable, function or closure isn’t in a scope, you can’t access it. Scope is sometimes called “context”.

Compare it to the “context” in which your house keys exist. When you’re outside the house, and your keys are inside the house, they’re not in the same context, and you can’t unlock the front door. When both you and the house keys are outside the house, you’re in the same context, and you can unlock the door.

Your code has global scopes and local scopes. Some examples:

  • A property, as defined within a class, is part of the global class scope. Anywhere within that class you can set and get the value of the property.
  • A variable, as defined in a function, is part of the local function scope. Anywhere within that function you can set and get the value of the property.

Want to learn more about scope and context? Check out this tutorial: Scope & Context Explained In Swift

Let’s look at an example of how a closure captures its surrounding scope.

let name = “Zaphod”

let greeting = {
print(“Don’t panic, \(name)!”)
}

greeting()

Here’s what happens in the code:

  • First, a constant name is defined with value “Zaphod” of type String.
  • Then, a closure is defined and assigned to greeting. The closure prints out some text.
  • Finally, the closure is executed by calling greeting().

In the example, the closure closes over the local variable name. It encapsulates variables that are available in the scope that the closure is defined in. As a result, we can access name even though it’s not declared locally within the closure!

A smart reader will now point out that name is accessible in the closure’s body because in a REPL environment, such as a sandbox or Swift Playground, top-level variables are part of the global scope. Yes, that’s right! You can access them anywhere. Please check the next example.

Let’s look at a more complex example:

func addScore(_ points: Int) -> Int
{
let score = 42

let calculate = {
return score + points
}

return calculate()
}

let value = addScore(11)
print(value)

What happens here?

  • First, a function addScore(_:) is defined. It will return a score based on the parameter points and the “previous” score of 42.
  • Then, within the function a closure calculate is defined. It simply adds score and points and returns the result. The function calls the closure with calculate().
  • Finally, the function addScore(_:) is called, assigned to value, and printed out.

The closure calculate captures both score and points! Neither of those variables are declared locally within the closure, yet the closure can get their values. That’s because of capturing.

See how there are different scopes? And see how the closure scope has access to score and points, that are part of the local function scope?

There are a few things worth noting here:

  1. A closure only captures variables etc. that are actually used in the closure. When a variable isn’t accessed in the closure, it isn’t captured.
  2. A closure can capture variables and constants. You can technically also capture other closures, because they’re variables or constants too, and properties, because a property belongs an object that’s assigned to a variable or constant. And you can of course call functions in a closure, but they aren’t captured, because functions aren’t variables (or “stored”).
  3. Capturing only works one way. The closure captures the scope it is defined in, but code “outside” a closure doesn’t have access to values “inside” the closure.

Capturing values with closures can lead to all sorts of fun with strong reference cycles and memory leaks. And that’s where the capture list comes in.

Strong References & Capture Lists

First things first. When a closure captures a value, it automatically creates a strong reference to that value. When Bob has a strong reference to Alice, then Alice isn’t removed from memory until Bob is removed from memory.

That usually goes OK. But what if Alice has a strong reference back to Bob? Then both Bob and Alice won’t be removed from memory, because they’re holding on to each other. Bob can’t be removed because Alice is holding onto him, and Alice can’t be removed because Bob is holding onto her.

This is called a strong reference cycle and it causes a memory leak. Imagine a hundred Bob’s and Alice’s taking up 10 MB each in memory, and you see what the problem is: less memory for other apps, and no way to remove it.

Memory in iOS is managed with a concept called Automatic Reference Counting. It’s different than garbage collection. Most of memory management with ARC is done for you, but you have’ll to avoid strong reference cycles. Memory management is a hairy subject, so we’ll leave that for another article.

You can break a strong reference cycle with a capture list. Just like you can mark a class’ property as weak, you can mark captured values in a closure as a weak or unowned reference. Their default is strong.

Here’s an example:

class Database {
var data = 0
}

let database = Database()
database.data = 11010101

let calculate = { [weak database] multiplier in
return database!.data * multiplier
}

let result = calculate(2)
print(result)

Here’s what happens in the code:

  • First, you define a class Database. It has one property data. It’s a fictional class, so imagine that it’s super memory intensive…
  • Then, you create an instance of Database, assign it to database, and set its data property to some integer value.
  • Then, you define a closure calculate. The closure takes one argument multiplier, and it captures database. Within the closure, the data is simply multiplied by multiplier.
  • Finally, the closure is called with argument 2 and its result is assigned to result.

The key part is the capture list, here:

{ [weak database]
A capture list is a comma-separated list of variable names, prepended with weak or unowned, and wrapped in square brackets. Some examples:

[weak self]
[unowned navigationController]
[unowned self, weak database]
You use a capture list to specify that a particular captured value needs to be referenced as weak or unowned. Both weak and unowned break the strong reference cycle, so the closure won’t hold on to the captured object.

Here’s what they mean:

  • The weak keyword indicates that the captured value can become nil
  • The unowned keyword indicates that the captured value never becomes nil

Both weak and unowned are the opposite of a strong reference, with the difference that weak indicates a variable that can become nil at some point.

You typically use unowned when the closure and the captured value will always refer to each other, and will always be deallocated at the same time. An example is [unowned self] in a view controller, where the closure will never outlive the view controller.

You typically use weak when the captured value at some point becomes nil. This can happen when the closure outlives the context it was created in, such as a view controller that’s deallocated before a lengthy task is completed. As a result, the captured value is an optional.

The concept of capturing, capture lists and memory management is tricky. It’s not that complicated, but I think it’s just hard to visualize such an abstract concept. In practical iOS development, the most common capture list is [weak self] or [unowned self].

As you’re reading this, just take note of the concepts and refer back to them whenever you get into trouble with memory management. Xcode will tell you when you’re using self in a closure, and if you do, that might be a good time to read up on the exact inner workings of capture lists.

Here’s what you learned so far:

  • A closure can capture its surrounding scope, making variables and constants from that scope accessible within the closure
  • Variables and constants are captured with a strong reference by default, which can cause a strong reference cycle
  • You can break the strong reference cycle with a capture list, by explicitly marking captured values as weak and unowned

Let’s move on to the next and last section about completion handlers!

Closures In Action: Completion Handlers

So what do you actually use closures for? They are incredibly powerful tools, but if you can’t put them to use, they won’t do you much good.

A common application of a closure is the completion handler. It works roughly like this:

  • You’re executing a lengthy task in your code, like downloading a file, making a calculation, or waiting for a webservice request
  • You want to execute some code when the lengthy task is completed, but you don’t want to “poll” the task continuously to check if it’s finished
  • Instead, you provide a closure to the lengthy task, which it will call when the task is completed (hence “completion handler”)

Here’s an example:

let task = session.dataTask(with: “http://example.com/api”, completionHandler: { data, response, error in

// Do something…
})
In the above code we’re making a networking request to download some data and do something with that data when the request is completed.

That may not seem shocking, so here’s the kicker:

  • We’re starting the networking request at point A
  • The completion handler is executed at point B (in the future)
  • We’re defining the completion handler at point A in the code
  • The completion handler can capture the scope at point A (in the past?)

It’s like time travel! You can code as if there’s no passing of time between starting the request and its completion. Instead of waiting for the lengthy task to complete, we can just provide a bit of code that’s executed when the task completes, while we’re coding the networking request in the here-and-now!

When you pass a closure as an argument for a function, and when that closure outlives the function it was passed to, it is said to be an escaping closure. Since Swift 3, closures are non-escaping by default. When you declare a function that takes a closure, and when that closure is escaping, its parameter has to be marked with @escaping. Read more about @escaping here.

Here’s what happens:

  • A lengthy task starts, and we’re defining a completion handler. The completion handler is a closure, so it captures variables in its surrounding scope.
  • The lengthy task completes, and your completion handler is executed. The closure scope is kept around, so we can use any variables and constants defined in the closure’s scope.

The key part is this: we can code the completion handler at the same time we’re starting the lengthy task!

Imagine you want to use the data of a networking request to display an image:

let imageView = UIImageView()

HTTP.request(“http://imgur.com/kittens”, completionHandler: { data in
imageView.image = data
})

See what happens here? You’re defining an image view, starting the networking request, and providing a completion handler. The completion handler is executed when the lengthy task is completed.

The closure has captured a reference to imageView, so you can set the image data when the lengthy task is completed. This happens in the future, but we’re still in the present! You can code it as if the lengthy task executes instantly.

Let’s compare that to the target-action pattern, another common approach to invoke functions at a later point in time.

func download()
{
let imageView = UIImageView()

Webservice.request(“http://imgur.com/kittens”, target: self, action: #selector(onDownloadComplete(_:)))
}

func onDownloadComplete(_ data: UIImage)
{
// Oh no! How do I get to the `imageView` from here?
}

See how that’s different?

Even though you can use target-action to determine what happens when a lengthy task completes, you don’t have the benefits of capturing, so you can’t respond to the completion in the here-and-now.

The smart reader now points out that you can make imageView an instance property, so you can access it in onDownloadComplete(_:). Yes, that’s a good alternative! But what if you need access to multiple variables? You don’t want to add unnecessary clutter. At best, the completion handler is in a different function, in a different place.

OK, before we call it quits, let’s look at one last bit of magic with closures. Consider the following code:

func lengthyTask(completionHandler: (Int) -> Int)
{
let result = completionHandler(42)
print(result)
}

lengthyTask(completionHandler: { number in
print(number)
return 101
})

Try it! What’s happening there?

Try it! What’s happening there?

  • First, we define a function lengthyTask(completionHandler:). When that function is executed, the completion handler is executed with one argument 42. The completion handler also returns a result, which is printed out.
  • Then, the function lengthyTask(completionHandler:) is called, and provided with a closure. The closure has one argument, as defined earlier, which is printed out with print(number), and it also returns a value 101.

As a result, the first print() will print out 101, and the second print() will print out 42. Wait a minute…

Is the closure providing a value back to the function that calls the closure? YES! It’s magical…

Because the closure is executed in lengthyTask, but defined earlier, you can return values from your closure just like any other function. The return value ends up with whomever called the closure!

Further Reading

And that, dear developer, is how closures work. Pfew! Quite fascinating and thrilling, right? It’s mind-boggling!

Here’s what you learned:

  • How closures work in Swift and how you can use them
  • The syntax for declaring and calling closures
  • How to compose and deconstruct a closure’s type
  • What “closing over” means and how to use a capture list
  • The single best use for closures: the completion handler
  • … and a whole bunch of stuff in-between


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts