Welcome to Part 2 of our Swift Programming Tutorial series! Now we will take you a little further from the basic understanding of Swift, and explore some important and advanced concepts that will help you write more powerful and efficient code.
Just like we covered basic concepts in Part 1, in this part too we will provide theory along with real-life examples and practical exercises, which will make it easier for you to understand complex topics. Whether you are a beginner or at the intermediate level, this tutorial will help you take your Swift skills to the next level.
By the end of Part 2, you will have a good command over the essential concepts of Swift, and you will be ready for the advanced topics ahead.
Come on, let’s get started and make your Swift journey more interesting!
In Part 2, we will build your foundation by diving into the basics of Swift:
Closure
Closures are a powerful and flexible concept in Swift that lets you define reusable code blocks. These are like functions but a little lighter and you can pass them as arguments, return from other functions, or capture values from their surrounding context.
In simple words, closures are small and independent code blocks that can be executed at any time. You can use this to handle asynchronous code, pass functionality, or make your code concise and clean.
{ (parameters) -> returnType in
// code block
}
- Parameters These are the values you pass when calling the closure. These are just like parameters in functions.
- ReturnType This is the type that the closure will return. This is optional — if the closure does not return any values, you do not need to specify anything.
- The IN keyword signals the start of a code block.
Types of Closures
Closures in Swift can be categorized in three ways:
- Global Functions: These are closures that are defined globally with a name (like a normal function).
func addNumbers(a: Int, b: Int) -> Int {
return a + b
}
- Nested Functions: These are closures that are inside a function.
func outerFunction() {
func innerFunction() {
print("This is a nested function")
}
innerFunction()
}
- Unnamed Closures (Inline Closures): These are anonymous and are passed as arguments.
let sum = { (a: Int, b: Int) -> Int in
return a + b
}
Removing parameter types and return types
If Swift can do type inference, parameter types and return types can be removed:
let greet = { name in
return "Hello, \(name)!"
}
Removing the RETURN keyword
If the closure returns a single expression, the RETURN keyword can also be removed:
let greet = { name in
"Hello, \(name)!"
}
Using Shorthand Argument Names
Parameters can be accessed via $0, $1, etc.:
let greet = { "Hello, \($0)!" }
// code
let result = greet("John")
print(result)
// Output "Hello John"
Closures as function arguments
Closures can be passed as arguments to a function:
func performOperation(_ operation: (Int, Int) -> Int) {
let result = operation(5, 3)
print("Result: \(result)")
}
performOperation { $0 + $1 } // Output: Result: 8
performOperation { $0 * $1 } // Output: Result: 15
Escaping Closures
An escaping closure is a closure that is executed outside the scope of the function in which it is passed. This means the closure is called after the function returns.
This happens typically when you pass a closure to a function that executes asynchronously (e.g., a network request).
To declare an escaping closure, you use the @escaping
keyword.
func fetchData(completion: @escaping () -> Void) {
DispatchQueue.global().async {
// Simulate some work
print("Fetching data...")
completion()
}
}
fetchData {
print("Data fetched!")
}
//OUPUT: Fetching data...
Data fetched!
@escaping
is used because the closure is called after the function fetchData
completes its execution (in the background, asynchronously).
DispatchQueue.global().async
starts a background task to simulate data fetching. After the background work is done, the closure completion()
is called, printing "Data fetched!"
.
Non-Escaping Closure
A non-escaping closure is a closure that executes within the function and completes before the function returns. Non-escaping closures are the default in Swift.
When the closure is called immediately, within the same scope of the function, it’s considered non-escaping.
func performAction(action: () -> Void) {
print("Action performed")
action() // Immediately CAll
print("Action performed done")
}
performAction {
print("Action is Performing")
}
//Output Action performed
Action is Performing
Action performed done
Here the performAction function accepts a closure action. This closure is non-escaping, meaning this closure is being executed within the scope of the function and is completed before the function returns.
@escaping is not used here because the closure is being executed immediately.
When to Use a Closure vs a Function?
- Use a function when:
- You need a reusable block of code that you can call multiple times.
- You need named and structured code.
- You don’t need to capture any values from outside the function.
- Use a Closure when:
- You need inline functionality that is short and concise.
- You need to capture values from the surrounding context.
- You are working with asynchronous tasks, like network requests, where the code will run later (escaping closures).
Protocol
Protocols are a powerful feature in Swift that help you organize code and increase reusability. Protocols let you define a blueprint that specifies what properties and methods an object should implement. It is a kind of contract that tells a class, struct, or enum what methods or properties it should have.
Today we will understand in detail what protocols are, how they work, and how they can be used practically.
// Defining a protocol
protocol Drivable {
var speed: Int { get set } // Property
func drive() // Method
}
// Class conforming to the protocol
class Car: Drivable {
var speed: Int = 0
func drive() {
print("The car is driving at \(speed) km/h.")
}
}
// Using the protocol
let myCar = Car()
myCar.speed = 60
myCar.drive() // Output: The car is driving at 60 km/h.
Here we have created a protocol named Drivable, which defines that any object which adopts this protocol should have speed property and drive() method.
Define properties and methods in a protocol:
- Properties can be defined with get set keywords.
- Methods are just declared, their implementation has to be done by the class or structure to be adopted after the protocol.
Multiple Protocols ka Adoption:
A class or struct can adopt more than one protocol.
protocol Flyable {
func fly()
}
class Airplane: Drivable, Flyable {
var speed: Int = 0
func drive() {
print("Airplane is driving.")
}
func fly() {
print("Airplane is flying at \(speed) km/h.")
}
}
Protocol Methods with Default Implementations:
Sometimes you may want a default implementation of the methods of a protocol, without the need to implement the class or structure that adopts it. You can do this through extensions.
protocol Eatable {
func eat()
}
extension Eatable {
func eat() {
print("Eating food.")
}
}
class Person: Eatable {
// No need to implement 'eat' method, default implementation from extension will be used.
}
let person = Person()
person.eat() // Output: Eating food.
Protocol with Associated Types:
If you need to use a type in a protocol that is generic, you can use associated types. This gives you flexibility in the protocol.
protocol Container {
associatedtype Item
var items: [Item] { get set }
mutating func addItem(_ item: Item)
}
struct Box: Container {
var items: [String] = []
mutating func addItem(_ item: String) {
items.append(item)
}
}
var myBox = Box()
myBox.addItem("Apple")
print(myBox.items) // Output: ["Apple"]
Here we created a Container protocol, which has an associated type of Item. Then the Box structure adopted this protocol and implemented the function to add items in it.
Extension
Extensions are a powerful feature in Swift that lets you extend existing classes, structs, enums, and protocols without modifying their original code. This feature helps you to make your code modular and reusable, and whenever you want to add or modify the behavior of a class or structure without directly changing its source code, then you use an extension.
class AddFund {
}
extension AddFund {
// New methods, properties, or initializers go here
}
Here AddFund can be any existing class, struct, enum, or protocol. You can define new methods, computed properties, and initializers within the extension.
Extension Example in Swift
Let’s look at a simple example where we will extend the String class and add a new method reverse() which will reverse the string.
// Extension for String class
extension String {
func reverse() -> String {
return String(self.reversed())
}
}
// Using the extension method
let newString = "Inspire, Dev!"
let reverse = newString.reverse()
print(reverse) // Output: "!veD ,eripsnI"
Here we extended the String class and added the reverse() method, which reverses the string.
Using Extensions with Protocols
In Swift you can use extensions to make a class conform to a protocol. This allows you to make your class part of a separate protocol without modifying its original implementation.
protocol Pirates {
var name: String { get }
}
extension String: Pirates {
var name: String {
return "JACK SPARROW \(self.count)"
}
}
// Using the protocol conformance
let ship = "BLACK PEARL"
print(ship.name) // Correct Output: JACK SPARROW 11
The string "BLACK PEARL"
has 11 characters, so the computed property name
will return the string "JACK SPARROW 11"
.
Limitations of Extensions
Cannot Override Existing Methods:
You cannot override an existing method through extensions. If a class already has a method, you cannot change its behavior.
Cannot Add Stored Properties:
Extension se aap stored properties nahi add kar sakte. Aap sirf computed properties ya methods add kar sakte ho.
No Deinitializers:
You cannot add Deinitializers to extensions.
Error Handling
Error handling is an important concept used in Swift so that we can handle unexpected situations or errors in our program. Meaning, when something goes wrong (like a network issue or invalid input), we can prevent the code from crashing and handle that error.
Error handling in Swift is quite structured and quite easy to use. In this post, we will understand error handling in detail with simple examples.
Types Of Error Handling
There can be 3 types of errors in Swift:
- Compile-Time Errors: These errors occur when the code is being compiled. These are caught by the Swift compiler.
- Runtime Errors: These errors occur when the app is running (such as accessing an out-of-bounds index in an array).
- Custom Errors: We can define these errors ourselves according to our code using the Error protocol.
How does error handling work?
For error handling in Swift, we need to use some key components:
- Error Type: We define the types of errors that can occur, for which we create enums or structures that conform to the error protocol.
- Throwing an Error: When an error occurs, we use the throw keyword to throw the error.
- Catching an Error: We catch and handle errors in a try-catch block.
Syntax Of Error Handling
- Throwing a Function: If a function can throw an error then it needs to be marked with throws.
- Catching an Error: We handle errors by using do, try, and catch.
// Define a custom error type
enum MyError: Error {
case networkError
case invalidData
}
// A function that throws an error
func fetchData(from url: String) throws {
if url.isEmpty {
throw MyError.invalidData
}
// Simulating network error
let success = false
if !success {
throw MyError.networkError
}
print("Data fetched successfully!")
}
// Handling errors using do-try-catch
do {
try fetchData(from: "https://inspire.com")
} catch MyError.networkError {
print("Network Error: Failed to fetch data.")
} catch MyError.invalidData {
print("Invalid Data Error: URL is empty.")
} catch {
print("An unexpected error occurred: \(error).")
}
// Output : Network Error: Failed to fetch data.
- Define the Error Type: First, we define an error type (using enum or struct) that conforms to the Error protocol.
- Throw the Error: When an error occurs, we use the throw keyword to throw the error.
- Catch and Handle the Error: The error is caught using the do-try-catch block, and we handle the error in the catch block.
Important Keywords in Error Handling
- throws: Used to mark a function that can throw an error.
- try: Used to call a function that may throw an error. You must handle the error with try or by catching it.
- do-try-catch: The block where errors are handled.
Access Control
Access control means how you allow access to parts of your code. With access control in Swift, you decide whether a class, structure, function, or variable can be accessed from external code or not.
There are 5 main access control levels in Swift:
open
public
internal (default)
fileprivate
private
Each level has its own scope and accessibility rules. This gives you control over what level of exposure parts of your code should be exposed.
open
- This is the highest-level access control.
- Classes and methods can also be inherited from other modules (libraries).
- You have to use open access modifier to extend the code for external code.
public
- This access control is also available for external code, but you do not get inheritance permission.
- A public class or function can be accessed from other modules, but it cannot be inherited.
internal
- This is the default access control if you do not use any access modifiers.
- Internal members are accessible only within the same module, but cannot be accessed from other modules.
- The most common level occurs when you want to use the code only within your module.
fileprivate
- The fileprivate access modifier is used to make access to members only within that file.
- If you want a class or variable to be accessible only within a file, then you can use fileprivate.
private
- This is the most restrictive access control.
- Private members are accessible only within the class or structure in which they are defined.
Conclusion
In Part 2 of our Swift programming journey, we laid a solid foundation by covering key concepts like Closure , Protocol, Extension, Error Handlig. These core topics help you write safe, efficient, and organized Swift code — essential for everyday development.
Now, as we move into Part 3, we’re taking a step further into the more advanced and powerful features of Swift! 🚀 Data Structure(Arrays, Dictionary, Sets), Generics, Memory Management and ARC, Concurrency and Multithreading (Async-Await).
In this part, we’ll unlock some game-changing tools that every Swift developer needs to know to build scalable, robust, and flexible applications:
Leave a Reply