Welcome to Part 4 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 4, 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 4, we will build your foundation by diving into the basics of Swift
Protocol-Oriented Programming (POP)
Protocol-Oriented Programming (POP) in Swift means that we make more use of protocols to make the code reusable and flexible. This is a feature of Swift which is quite different from Object-Oriented Programming (OOP). Protocols are basically a blueprint that explains what a class, struct, or enum should do. Structures and enums in Swift can also adopt protocols, which greatly reduces the dependency of inheritance.
Why POP Over OOP?
- Multiple Inheritance Problem Solved: In POP a structure or class can adopt multiple protocols.
- Working with Structs: In OOP inheritance happens only with classes, but in POP structs and enums can also follow the protocols.
- Reusable and Modular Code: The focus of POP is to write small protocols that can be reused in different places.
Protocol Extensions with Default Implementations
Protocol extensions are a powerful feature in Swift, in which we can define the default behavior of methods of protocols. This eliminates the need to provide separate implementations for each type.
protocol Washable {
func wash()
}
extension Washable {
func wash() {
print("Washing the object by default.")
}
}
struct Car: Washable {}
struct Bike: Washable {
func wash() {
print("Washing the bike with special care.")
}
}
// Usage
let car = Car()
car.wash() // Output: Washing the object by default.
let bike = Bike()
bike.wash() // Output: Washing the bike with special care.
- Car did not provide a wash() method, so it got the default implementation.
- The bike provided its own custom implementation, so it got its own behavior calls.
Real-World Example: Equatable and Comparable Protocols
Using Swift’s built-in protocols like Equatable and Capable, we can provide comparison or equality behavior to our custom types.
Using Equatable Protocol
struct Person: Equatable {
let name: String
let age: Int
}
let person1 = Person(name: "Alice", age: 25)
let person2 = Person(name: "Bob", age: 30)
let person3 = Person(name: "Alice", age: 25)
// Check Equality
print(person1 == person2) // Output: false
print(person1 == person3) // Output: true
- A table automatically generates the behavior of the == operator if your properties are a table.
Using Comparable Protocol
struct Product: Comparable {
let name: String
let price: Double
static func < (lhs: Product, rhs: Product) -> Bool {
return lhs.price < rhs.price
}
}
let product1 = Product(name: "iPhone", price: 999.99)
let product2 = Product(name: "iPad", price: 799.99)
print(product1 < product2) // Output: false
By adopting Comparable, we define custom behavior for <, >, and other cursor operators.
Reusable Protocol for Networking
By adopting Comparable, we define custom behavior for <, >, and other cursor operators.
let greet = { name in
return "Hello, \(name)!"
}
Reusable Protocol for Networking
For networking, we write a Russable protocol that handles HTTP requests.
Step 1
protocol NetworkRequest {
func fetch(from url: String, completion: @escaping (Result<Data, Error>) -> Void)
}
extension NetworkRequest {
func fetch(from url: String, completion: @escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: url) else {
completion(.failure(NSError(domain: "Invalid URL", code: 400, userInfo: nil)))
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(data ?? Data()))
}
}.resume()
}
}
step 2
struct APIService: NetworkRequest {}
let service = APIService()
service.fetch(from: "https://jsonplaceholder.typicode.com/posts") { result in
switch result {
case .success(let data):
print("Data fetched successfully: \(data)")
case .failure(let error):
print("Error occurred: \(error.localizedDescription)")
}
}
- NetworkRequest is a protocol that defines the blueprint for handling HTTP requests.
- The default implementation of fetch is given in the Protocol extension.
- APIService just has to adopt the protocol, and all the functionality becomes available automatically.
Property Wrapper
Property Wrappers in Swift are a feature that lets you customize the behavior of properties without repeating common logic.
For example, if you want to automatically validate the value of a property or provide default behavior, you can use Property Wrappers. Property Wrappers create reusable logic that can be applied to different properties.
Key Benefits of Property Wrappers
- Code Reusability: Write common logic in one place and reuse it for multiple properties.
- Encapsulation: Neatly wraps a property’s behavior so that it does not have to be repeated for every property.
- Ease of Use: You can easily apply property wrappers using @ syntax.
Basic Syntax of Property Wrappers
Step 1: Define a Property Wrapper
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
}
- ValuedValue: The actual value of the property is stored here.
- NewValue.Capitalized: Every time a value is assigned, it is capitalized.
Step 2: Use the Property Wrapper
struct User {
@Capitalized var name: String
}
var user = User()
user.name = "john doe"
print(user.name) // Output: John Doe
@Capitalized:
The property wrapper defines that whenever a value is assigned to the name property, it will be capitalized.
Default Behavior:
wrappedValue provides a getter and setter that defines the behavior of the property each time.
Reusability:
You can use this wrapper with any string property.
Built-in Property Wrappers in Swift
@State
in SwiftUI
@State in Swift is used to make it reactive properties.
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
@Published
in Combine
In the Combine framework, @Published stands for Observable properties.
import Combine
class Counter: ObservableObject {
@Published var count = 0
}
let counter = Counter()
let subscription = counter.objectWillChange.sink {
print("Count will change to: \(counter.count)")
}
counter.count = 1 // Prints: Count will change to: 1
Conclusion
Property Wrappers are a powerful tool for making code concise, reusable, and flexible in Swift. These provide the best solution for encapsulation and reusability, especially when you need consistent behavior for different properties.
- You can use these to define real-world examples such as clamping values, persisting data, and default behavior.
- Swift’s built-in wrappers (@State, @Published) and custom wrappers both increase developer productivity!
Codable (Serialization and Deserialization)
Codable in Swift is a protocol that makes it very easy to work with JSON and external formats. If you need to fetch JSON data from APIs or convert a Swift object into JSON, then Codable is very useful.
Codable is an umbrella protocol that combines two protocols:
- Encodable: To encode Swift objects (e.g., to create JSON).
- Decodable: To decode JSON or external data (e.g., to create a Swift object).
How to Use Codable?
Basic JSON Parsing
{
"id": 101,
"name": "John Doe",
"email": "john@example.com"
}
Step 1: Create a Swift Struct
struct User: Codable {
let id: Int
let name: String
let email: String
}
Step 2: Decode JSON into Swift Object
let jsonString = """
{
"id": 101,
"name": "John Doe",
"email": "john@example.com"
}
"""
if let jsonData = jsonString.data(using: .utf8) {
do {
let user = try JSONDecoder().decode(User.self, from: jsonData)
print(user.name) // Output: John Doe
} catch {
print("Failed to decode JSON: \(error)")
}
}
Step 3: Encode Swift Object to JSON
let user = User(id: 101, name: "John Doe", email: "john@example.com")
do {
let jsonData = try JSONEncoder().encode(user)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
// Output: {"id":101,"name":"John Doe","email":"john@example.com"}
}
} catch {
print("Failed to encode JSON: \(error)")
}
Customizing Serialization Using CodingKeys
If the keys of the objects are different from Swift properties, then you use CodingCase.
Example: JSON with Different Key Names
{
"user_id": 101,
"full_name": "John Doe",
"user_email": "john@example.com"
}
Swift Struct
struct User: Codable {
let id: Int
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id = "user_id"
case name = "full_name"
case email = "user_email"
}
}
The process of decoding and encoding will be the same, only the coding case defines the mapping between objects and Swift properties.
Error Handling with Codable
Decoding may fail if the format is incorrect or the fields in the structure do not match. You can handle errors by using two catch blocks.
do {
let user = try JSONDecoder().decode(User.self, from: jsonData)
print(user)
} catch DecodingError.keyNotFound(let key, let context) {
print("Missing key: \(key.stringValue) - \(context.debugDescription)")
} catch DecodingError.typeMismatch(let type, let context) {
print("Type mismatch: \(type) - \(context.debugDescription)")
} catch {
print("Decoding failed: \(error.localizedDescription)")
}
Conclusion
Swift’s Codable protocol makes JSON parsing very easy and efficient. You can use it:
- You can handle different key names of JSON (CodingKeys).
- Can decode nested and complex JSON structures.
- You can decode and encode JSON arrays.
Concurrency with async/await
Older Techniques: Closures and DispatchQueue
Earlier in Swift, closures or DispatchQueue were used to write asynchronous code. These techniques were complicated and error-prone, especially when multiple tasks had to be handled.
DispatchQueue.global().async {
// Background work
let result = "Hello, World"
DispatchQueue.main.async {
// Update UI on main thread
print(result)
}
}
In this you have to manually handle that the code runs on the background thread and UI updates happen on the main thread. This is a lot of boilerplate code and a bit tough to manage, especially when nested callbacks (closures) happen.
Modern async/await (Swift 5.5 and later)
With async/await, asynchronous programming in Swift has become much cleaner and readable. When calling async functions, you use the await keyword, which tells the compiler that the function is running asynchronously and that you must wait for the result.
func fetchData() async -> String {
// Simulating background work
return "Hello, World"
}
Task {
let result = await fetchData()
print(result) // Output: Hello, World
}
- Cleaner Syntax: Async/await makes code appear directly sequential, like synchronous code.
- No need for explicit dispatching: There is no tension of handling background thread and main thread like DispatchQueue.
Writing Concurrent Code with Task and TaskGroup
Tasks and TaskGroups have been introduced in Swift 5.5, which is the best method to handle concurrent tasks.
Using Task for Simple Concurrency
Tasks allow you to run asynchronous code in the background.
func fetchData1() async -> String {
return "Data 1"
}
func fetchData2() async -> String {
return "Data 2"
}
Task {
let data1 = await fetchData1()
let data2 = await fetchData2()
print(data1)
print(data2)
}
Here by using Task we are handling parallel asynchronous functions. Both fetchData1() and fetchData2() are executed simultaneously, and when the result is ready, it is printed.
Using TaskGroup for Concurrent Tasks with Results
If you want to run multiple tasks concurrently and aggregate their results, you use taskgroups.
func fetchData1() async -> String {
return "Data 1"
}
func fetchData2() async -> String {
return "Data 2"
}
Task {
await withTaskGroup(of: String.self) { group in
group.addTask {
return await fetchData1()
}
group.addTask {
return await fetchData2()
}
for await result in group {
print(result)
}
}
}
withTaskGroup
: Yeh function multiple tasks ko concurrently run karne ke liye use hota hai. Har task ka result collect karke ek sath process kiya ja sakta hai.
Conclusion
In Part 4 of our Swift programming journey, we laid a solid foundation by covering key concepts like Protocol-Oriented Programming , Property Wrapper, Codable, Concurrency with async/await. These core topics help you write safe, efficient, and organized Swift code — essential for everyday development.
Now, as we move into Part 5, we’re taking a step further into the more advanced and powerful features of Swift!
Leave a Reply