Welcome to Part 3 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 intermediate concepts in Part 2, 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 3, 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 3, we will build your foundation by diving into the basics of Swift:
Generics
Generics in Swift means that we can write functions or classes that can work with any type. Suppose there is a function that compares numbers. If we need the same logic for strings, then we don’t need to write a separate function. This is what Generics do.
What is its advantage?
- The same code will work with multiple types.
- It is type safe – if you pass the wrong type, you will get a compile-time error.
Example
func findMaximum(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
// Usage
let maxInt = findMaximum(10, 20) // Works with Int
print("Maximum Int: (maxInt)") // Output: 20
let maxDouble = findMaximum(3.5, 7.8) // Works with Double
print("Maximum Double: (maxDouble)") // Output: 7.8
let maxString = findMaximum("Apple", "Banana") // Works with String
print("Maximum String: (maxString)") // Output: Banana
T: Comparable
ensures that the type supports comparison (using<
or>
).- Function can now handle numbers and strings without rewriting the logic.
Benefits of Generics:
- Reusability: One code is used for different data types.
- Type Safety: Errors are caught at compile-time itself.
- Code Simplification: There is no need to write duplicate code.
Higher Order Function
A higher-order function is a function that works with a function—either takes input from a function or gives output to the function.
Accept one or more functions as parameters, or return output as a single function.
Function as Parameter (Input)
Suppose there is a function that takes another function as a parameter:
func operateOnNumbers(_ a: Int, _ b: Int, using operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
- operateOnNumbers is a higher-order function.
- In this operation is a parameter which is a function.
- You can pass any custom logic as an operation.
Example 2: Function as Return Value (Output)
A higher-order function that returns another function
func makeMultiplier(by factor: Int) -> (Int) -> Int {
return { number in
return number * factor
}
}
// Example usage:
let double = makeMultiplier(by: 2)
print(double(5)) // Output: 10
let triple = makeMultiplier(by: 3)
print(triple(5)) // Output: 15
MakeMultiplier is a higher-order function that returns a function.
The function it returns multiplies numbers.
In Swift you get some built-in higher-order functions such as:
map
The job of map is to transform each element and return a new array.
let numbers = [1, 2, 3, 4, 5]
// Using map to square each number
let squaredNumbers = numbers.map { $0 * $0 }
print("Squared Numbers: \(squaredNumbers)") // Output: [1, 4, 9, 16, 25]
filter
The function of a filter is to filter elements based on a condition.
let numbers = [1, 2, 3, 4, 5]
// Using filter to get even numbers
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print("Even Numbers: \(evenNumbers)") // Output: [2, 4]
The filter returns only those elements that satisfy the closure condition ($0 % 2 == 0 means even numbers).
reduce
The function of Reduce is to combine elements of a collection to create a single value.
let numbers = [1, 2, 3, 4, 5]
// Using reduce to calculate sum
let sum = numbers.reduce(0) { $0 + $1 }
print("Sum: \(sum)") // Output: 15
- The first parameter (0) of Reduce is the initial value.
- In the closure { $0 + $1 } $0 is the accumulator (current result) and $1 is the current element.
Benefit of Higher Order Function
- Reusable Code: The same logic is used for different purposes.
- Concise Syntax: Less code is required to write operations on Collections.
- Customizability: Functions can be customized with closures.
Memory Management
Memory management means efficiently allocating and deallocating memory resources (like RAM) while a program is running. If memory is not handled properly, the program may slow down, crash, or use unnecessary memory.
Swift uses ARC (Automatic Reference Counting) for automatic memory management. ARC ensures that memory is allocated only when needed, and is freed when the need is over.
How does ARC work?
- When you create an object (instance), ARC allocates memory for that object.
- ARC tracks the reference count, which tells how many variables or properties are referring to that object.
- When the reference count becomes 0 (meaning no one is using the object), ARC frees the memory for that object.
Strong, Weak, aur Unowned References
Strong Reference:
The default behavior is: When a variable strongly refers to an object, its reference count is incremented.
var person1 = Person(name: "John") // Reference count: 1
var person2 = person1 // Reference count: 2
person1 = nil // Reference count: 1
Weak Reference:
When you use a reference, it does not increase the reference count. This is to avoid circular references.
class Person {
let name: String
init(name: String) {
self.name = name
}
}
class Apartment {
weak var tenant: Person? // Weak reference
}
var john: Person? = Person(name: "John")
var flat = Apartment()
flat.tenant = john
john = nil // Object deallocated kyunki weak reference ne object hold nahi kiya
Unowned Reference:
When you use weak reference, it does not increase the reference count. This is used to avoid circular references when you are sure that the reference will never be nil, then you use unowned reference. This avoids memory leaks but if the object gets deallocated and you access it, it can lead to crash. This is what happens.
class CreditCard {
unowned let owner: Person // Unowned reference
init(owner: Person) {
self.owner = owner
}
}
var john: Person? = Person(name: "John")
var card = CreditCard(owner: john!)
john = nil // Card ka owner access karne ki koshish karoge to crash hoga
Memory Management Tips in Swift
- Understanding ARC: Understand the use of strong, weak, and unowned references.
- By using [weak self] in closures: closures can create retain cycles. Use [weak self] to avoid this.
class ViewController {
var name = "Hello"
func printName() {
DispatchQueue.global().async { [weak self] in
print(self?.name ?? "No Name")
}
}
}
Conclusion
Swift’s ARC system makes memory management easier, but it is the developer’s responsibility to handle retain cycles. Proper use of strong, weak, and unowned references is important to avoid memory leaks and unnecessary memory usage.
Collection Type
Collection Types are data structures in Swift that store one or more values as a single unit. Using Collection types, you can manage multiple data values in an easy and organized way.
Swift me 3 primary collection types hote hain:
- Array
- Set
- Dictionary
Array
Array is an ordered collection that stores elements of the same type. The order of the elements is fixed, and you can access them through index.
- The order of the elements is maintained.
- Duplicate values are allowed.
- Zero-based indexing is used.
Create an Array
// Empty array
var numbers: [Int] = []
// Array with initial values
var fruits: [String] = ["Apple", "Banana", "Mango"]
// Type inference
var cities = ["Delhi", "Mumbai", "Kolkata"]
Access and Modify Elements
print(fruits[0]) // Output: Apple
fruits.append("Grapes") // Add a new element
fruits[1] = "Orange" // Update element at index 1
print(fruits) // Output: ["Apple", "Orange", "Mango", "Grapes"]
Other Useful Operations
print(fruits.count) // Number of elements
print(fruits.isEmpty) // Check if array is empty
fruits.remove(at: 2) // Remove element at index 2
print(fruits) // Output: ["Apple", "Orange", "Grapes"]
Set
A set is an unordered collection that stores unique values. Duplicate elements are not allowed.
- The order of the elements is random.
- Duplicate values are not allowed.
- Sets are used for fast searching and comparisons.
Create a Set
// Empty set
var numbers: Set<Int> = []
// Set with initial values
var fruits: Set<String> = ["Apple", "Banana", "Mango", "Apple"]
print(fruits) // Output: ["Mango", "Banana", "Apple"] (Order not fixed, duplicates removed)
Add and Remove Elements.
fruits.insert("Grapes") // Add a new element
fruits.remove("Banana") // Remove an element
print(fruits) // Output: ["Mango", "Grapes", "Apple"]
Loop Through Set
for fruit in fruits {
print(fruit)
}
// Output: Random order
Set Operations
let oddNumbers: Set = [1, 3, 5, 7]
let evenNumbers: Set = [2, 4, 6, 8]
let primeNumbers: Set = [2, 3, 5, 7]
// Union: Combine all elements
print(oddNumbers.union(evenNumbers)) // Output: [1, 2, 3, 4, 5, 6, 7, 8]
// Intersection: Common elements
print(oddNumbers.intersection(primeNumbers)) // Output: [3, 5, 7]
// Difference: Elements not in the other set
print(oddNumbers.subtracting(primeNumbers)) // Output: [1]
Here, the calculator(length:width:) function takes two parameters, length and width, and returns their product.
Dictionary
A dictionary is an unordered collection that stores key-value pairs. Each key is unique and has a corresponding value.
- is unique, but the values can be duplicated.
- This is used for fast data retrieval.
Create a Dictionary
// Empty dictionary
var students: [Int: String] = [:]
// Dictionary with initial values
var capitals: [String: String] = ["India": "Delhi", "USA": "Washington", "Japan": "Tokyo"]
Access and Modify Elements
print(capitals[“India”] ?? “Not Found”) // Output: Delhi
capitals[“UK”] = “London” // Add a new key-value pair
capitals[“India”] = “New Delhi” // Update existing value
print(capitals) // Output: [“USA”: “Washington”, “India”: “New Delhi”, “Japan”: “Tokyo”, “UK”: “London”]
Remove an Element
capitals["Japan"] = nil
print(capitals) // Output: ["USA": "Washington", "India": "New Delhi", "UK": "London"]
Loop Through Dictionary
for (country, capital) in capitals {
print("\(country): \(capital)")
}
// Output:
// USA: Washington
// India: New Delhi
// UK: London
Array: Jab order maintain karna ho aur duplicate values allow ho.
Set: Jab unique values chahiye aur fast lookup required ho.
Dictionary: Jab data ko key-value pair format me store karna ho.
Struct and Class
Structs and Classes in Swift are both user-defined data types that define their objects. But, there are many differences between the two.
Structs: These are value types, meaning when you copy an object of a struct, it becomes an independent copy. Meaning, when you assign a structure to another variable, a new copy of the data is created.
Classes: These are reference types, meaning when you copy an object of a class, both references point to the same memory location. Matlab, if you modify an object of a class, those changes are reflected in all references.
Struct Example in Swift:
When you use structs, you are dealing with value types. When you copy a struct object, it becomes an independent copy.
struct Point {
var x: Int
var y: Int
func description() -> String {
return "(\(x), \(y))"
}
}
// Struct ka object create karte hain
var point1 = Point(x: 5, y: 10)
print(point1.description()) // Output: (5, 10)
// point1 ko copy karte hain
var point2 = point1
point2.x = 20
print(point1.description()) // Output: (5, 10)
print(point2.description()) // Output: (20, 10)
Here, when we copy point1 to point2, both objects become independent. Meaning, if we change point2.x, there is no effect on point1.x. While copying structures, data is stored in a new memory location.
Class Example in Swift:
When you use classes you are doing reference type work. When you copy an object of a class, it points to the same memory location. Meaning, if you modify the class object, those changes are reflected in all the references.
class Car {
var model: String
init(model: String) {
self.model = model
}
func description() -> String {
return "Car model is \(model)"
}
}
// Class ka object create karte hain
var car1 = Car(model: "Tesla")
print(car1.description()) // Output: Car model is Tesla
// car1 ka reference car2 mein assign karte hain
var car2 = car1
car2.model = "BMW"
print(car1.description()) // Output: Car model is BMW
print(car2.description()) // Output: Car model is BMW
Here, when we assign car1 to car2, both are pointing to the same object. So, when we update car2.model to “BMW”, car1.model also changes, as both are sharing the same memory location.
When to Use Structs vs Classes?
Use Struct when you need lightweight data types that pass values, such as coordinates, ranges, and simple data models. Structs are best used when you need independent copies of objects while keeping performance in mind.
Example: Coordinates (x, y), size of a Rectangle (width, height), etc.
Use Class when you create complex objects that require shared state, inheritance, and mutability. Classes give you the flexibility to pass references to objects, which is important in large-scale applications.
Example: Model objects, UI components, network managers, etc.
Additional Concepts:
Inheritance in Classes:
Classes have the functionality of inheritance, which allows you to share properties and methods from one class to another. If you want to share common properties and methods, you can use inheritance.
class Animal {
var name: String
init(name: String) {
self.name = name
}
func speak() {
print("\(name) makes a sound")
}
}
class Dog: Animal {
func fetch() {
print("\(name) is fetching the ball!")
}
}
let myDog = Dog(name: "Buddy")
myDog.speak() // Output: Buddy makes a sound
myDog.fetch() // Output: Buddy is fetching the ball!
Here, the Dog class inherits the Animal class and uses the speak() method.
Deinitializers in Classes:
Classes have deinitializers, which are called when the object is released from memory. Structs do not have such a mechanism.
class House {
var address: String
init(address: String) {
self.address = address
}
deinit {
print("\(address) ka memory release ho gaya hai.")
}
}
var house1: House? = House(address: "123 Street")
house1 = nil // Output: 123 Street ka memory release ho gaya hai.
Here, when house1 is set to nil, the initializer is called and the memory is freed.
Conclusion
In Part 2 of our Swift programming journey, we laid a solid foundation by covering key concepts like Generics, Higher Order Function , Memory Management, Collection Type. These core topics help you write safe, efficient, and organized Swift code — essential for everyday development.
In this part 3, we’ll unlock some game-changing tools that every Swift developer needs to know to build scalable, robust, and flexible applications:
Leave a Reply