Autor: admin

  • Visual Regresion Testing: Implementing Snapshot test on iOS

    Visual Regresion Testing: Implementing Snapshot test on iOS

    Implementing snapshot tests alongside regular unit tests is valuable because it addresses an often-overlooked aspect of testing: UI consistency. While unit tests verify business logic correctness, snapshot tests capture and compare UI renderings, preventing unintended visual regressions. This is especially useful in dynamic UIs, where small code changes might break layouts without being detected by standard tests.

    By demonstrating how to integrate snapshot testing effectively, we aim to help developers enhance app stability, streamline UI testing, and adopt a more comprehensive test-driven approach.

    Setup iOS Project

    We willl creeate a regular iOS app project, with Swift  test target (not XCTest):

    To include the swift-snapshot-testing package in your project, add it via Swift Package Manager (SPM) using the following URL: https://github.com/pointfreeco/swift-snapshot-testing.

    Important: When adding the package, ensure you assign it to the test target of your project. This is necessary because swift-snapshot-testing is a testing framework and should only be linked to your test bundle, not the main app target.

    We will continue by implementing the views that will be validated:

    struct RootView: View {
        @State var navigationPath = NavigationPath()
    
        var body: some View {
            NavigationStack(path: $navigationPath) {
                ContentView(navigationPath: $navigationPath)
                    .navigationDestination(for: String.self) { destination in
                        switch destination {
                        case "SecondView":
                            SecondView(navigationPath: $navigationPath)
                        case "ThirdView":
                            ThirdView(navigationPath: $navigationPath)
                        default:
                            EmptyView()
                        }
                    }
            }
        }
    }
    
    struct ContentView: View {
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            VStack {
                Text("First View")
                    .font(.largeTitle)
                    .padding()
                
                Button(action: {
                    navigationPath.append("SecondView")
                }) {
                    Text("Go to second viee")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .accessibilityIdentifier("incrementButton")
            }
            .navigationTitle("First View")
        }
        
        func testButtonPress() {
            navigationPath.append("SecondView")
        }
    }
    
    struct SecondView: View {
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            VStack {
                Text("Second View")
                    .font(.largeTitle)
                    .padding()
                
                Button(action: {
                    navigationPath.append("ThirdView")
                }) {
                    Text("Go to third view")
                        .padding()
                        .background(Color.green)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }
            .navigationTitle("Second View")
        }
    }
    
    struct ThirdView: View {
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            VStack {
                Text("Third View")
                    .font(.largeTitle)
                    .padding()
                
                Button(action: {
                    // Pop to root
                    navigationPath.removeLast(navigationPath.count) // Empty stack
                }) {
                    Text("Get back to first view")
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }
            .navigationTitle("Third view")
        }
    }

    The provided SwiftUI code defines a navigation flow where a user can navigate through three views: FirstView, SecondView, and ThirdView. It uses a NavigationStack to manage the navigation path with a NavigationPath that is shared across views. In ContentView, the user can navigate to SecondView by pressing a button. In SecondView, the user can proceed to ThirdView with another button. In ThirdView, there is a button that clears the navigation stack, taking the user back to the FirstView. The navigation path is managed using the navigationPath state, and the specific view to navigate to is determined by the string values in the navigation path. 

    Snapshop testing

    napshot testing in iOS is a method that focuses on verifying the visual elements of an app’s user interface, such as fonts, colors, layouts, and images. It involves capturing a screenshot of the UI and saving it as a reference image, then comparing it pixel by pixel with new screenshots taken during subsequent tests. This technique allows developers to quickly detect unintended visual changes or regressions caused by code modifications, ensuring UI consistency across different versions of the app. By automating visual verification, snapshot testing complements other testing methods, such as unit and integration tests, by specifically addressing the app’s visual aspects and helping maintain a high-quality user experience.

    Swift Testing allows test functions to be parameterized. In our case, we will parameterize test functions based on the device screen we are interested in.

    protocol TestDevice {
        func viewImageConfig() -> ViewImageConfig
    }
    
    struct iPhoneSe: TestDevice  {
         func viewImageConfig() -> ViewImageConfig {
            ViewImageConfig.iPhoneSe
        }
    }
    
    struct iPhone13ProMax: TestDevice  {
         func viewImageConfig() -> ViewImageConfig {
            ViewImageConfig.iPhone13ProMax(.portrait)
        }
    }
    
    struct iPhone12Landscape: TestDevice  {
         func viewImageConfig() -> ViewImageConfig {
             ViewImageConfig.iPhone12(.landscape)
        }
    }

    For our sample, we will use an iPhone SE, iPhone 13 Pro Max, and iPhone 12 (in landscape mode). Finally, the test itself:

    @MainActor
    @Suite("Snapshot tests")
    struct SnapshotTests {
        
        var record = true // RECORDING MODE!
        static let devices: [TestDevice] = [iPhoneSe(), iPhone13ProMax(), iPhone12Landscape()]
        
        @Test(arguments: devices) func testFirstView(device: TestDevice) {
            let rootView = RootView()
            let hostingController = UIHostingController(rootView: rootView)
            var named = String(describing: type(of: device))
            assertSnapshot(of: hostingController,
                           as: .image(on: device.viewImageConfig()),
                           named: named,
                           record: record)
        }
        
        @Test(arguments: devices) func testSecondView(device: TestDevice) {
            let secondView = SecondView(navigationPath: .constant(NavigationPath()))
            let hostingController = UIHostingController(rootView: secondView)
    
            var named = String(describing: type(of: device))
            
            assertSnapshot(of: hostingController,
                           as: .image(on: device.viewImageConfig()),
                           named: named,
                           record: record)
        }
        
        @Test(arguments: devices) func testThirdView(device: TestDevice) {
            let thirdView = ThirdView(navigationPath: .constant(NavigationPath()))
            let hostingController = UIHostingController(rootView: thirdView)
    
            var named = String(describing: type(of: device))
            
            assertSnapshot(of: hostingController,
                           as: .image(on: device.viewImageConfig()),
                           named: named,
                           record: record)
        }
    }

    We have defined a test function for each screen to validate. On the first execution, var record = true, meaning that reference screenshots will be taken. Run the test without worrying about failure results.

    The important point is that a new folder called __Snapshots__ has been created to store the taken snapshots. These snapshots will serve as reference points for comparisons. Don’t forget to commit the screenshots. Now, switch record to false to enable snapshot testing mode

    ...
    var record = false // SNAPSHOT TESTING MODE!
    ...

    Run the test and now all must be green:

    Do test fail!

    Now we are going to introduce some changes to the view:

    Launch test: We now face issues where the tests validating ContentView are failing:

    When we review logs, we can see in which folder the snapshots are stored.

    With your favourite folder content comparator compare both folders:

    In my case, I use Beyond Compare, click on any file pair:

    With the image comparator included in BeyondCompare we can easily see with view components have changed.

    Conclusions

    Snapshot testing is a valuable complement to unit testing, as it enables you to detect regressions in views more effectively. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

    References

  • S.O.L.I.D. principles in Swift

    S.O.L.I.D. principles in Swift

    Applying SOLID principles to Swift is valuable because it enhances code quality, maintainability, and scalability while leveraging Swift’s unique features like protocols, value types, and strong type safety. Swift’s modern approach to object-oriented and protocol-oriented programming aligns well with SOLID, making it essential for developers aiming to write modular, testable, and flexible code.

    In this post we will revieew 5 principles by examples.

    S.O.L.I.D. principles

    The SOLID principles are a set of five design guidelines that help developers create more maintainable, flexible, and scalable software. These principles were introduced by Robert C. Martin, also known as Uncle Bob, and are widely adopted in object-oriented programming. The acronym SOLID stands for: Single Responsibility Principle (SRP)Open/Closed Principle (OCP)Liskov Substitution Principle (LSP)Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). Each principle addresses a specific aspect of software design, such as ensuring that a class has only one reason to change (SRP), allowing systems to be extended without modifying existing code (OCP), ensuring derived classes can substitute their base classes without altering behavior (LSP), creating smaller, more specific interfaces instead of large, general ones (ISP), and depending on abstractions rather than concrete implementations (DIP).

    By adhering to the SOLID principles, developers can reduce code complexity, improve readability, and make systems easier to test, debug, and extend. For example, SRP encourages breaking down large classes into smaller, more focused ones, which simplifies maintenance. OCP promotes designing systems that can evolve over time without requiring extensive rewrites. LSP ensures that inheritance hierarchies are robust and predictable, while ISP prevents classes from being burdened with unnecessary dependencies. Finally, DIP fosters loose coupling, making systems more modular and adaptable to change. Together, these principles provide a strong foundation for writing clean, efficient, and sustainable code.

    SRP-Single responsability principle

    The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented design, stating that a class or module should have only one reason to change, meaning it should have only one responsibility or job. This principle emphasizes that each component of a system should focus on a single functionality, making the code easier to understand, maintain, and test. By isolating responsibilities, changes to one part of the system are less likely to affect others, reducing the risk of unintended side effects and improving overall system stability. In essence, SRP promotes modularity and separation of concerns, ensuring that each class or module is cohesive and focused on a specific task.

    // Violating SRP
    class EmployeeNonSRP {
        let name: String
        let position: String
        let salary: Double
        
        init(name: String, position: String, salary: Double) {
            self.name = name
            self.position = position
            self.salary = salary
        }
        
        func calculateTax() -> Double {
            // Tax calculation logic
            return salary * 0.2
        }
        
        func saveToDatabase() {
            // Database saving logic
            print("Saving employee to database")
        }
        
        func generateReport() -> String {
            // Report generation logic
            return "Employee Report for \(name)"
        }
    }
    
    // Adhering to SRP
    class Employee {
        let name: String
        let position: String
        let salary: Double
        
        init(name: String, position: String, salary: Double) {
            self.name = name
            self.position = position
            self.salary = salary
        }
    }
    
    class TaxCalculator {
        func calculateTax(for employee: Employee) -> Double {
            return employee.salary * 0.2
        }
    }
    
    class EmployeeDatabase {
        func save(_ employee: Employee) {
            print("Saving employee to database")
        }
    }
    
    class ReportGenerator {
        func generateReport(for employee: Employee) -> String {
            return "Employee Report for \(employee.name)"
        }
    }

    In the first example, the Employee class violates SRP by handling multiple responsibilities: storing employee data, calculating taxes, saving to a database, and generating reports.

    The second example adheres to SRP by separating these responsibilities into distinct classes. Each class now has a single reason to change, making the code more modular and easier to maintain.

    OCP-Open/Close principle

    The Open/Closed Principle (OCP) is one of the SOLID principles of object-oriented design, stating that software entities (such as classes, modules, and functions) should be open for extension but closed for modification. This means that the behavior of a system should be extendable without altering its existing code, allowing for new features or functionality to be added with minimal risk of introducing bugs or breaking existing functionality. To achieve this, developers often rely on abstractions (e.g., interfaces or abstract classes) and mechanisms like inheritance or polymorphism, enabling them to add new implementations or behaviors without changing the core logic of the system. By adhering to OCP, systems become more flexible, maintainable, and scalable over time.

    protocol Shape {
        func area() -> Double
    }
    
    struct Circle: Shape {
        let radius: Double
        func area() -> Double {
            return 3.14 * radius * radius
        }
    }
    
    struct Square: Shape {
        let side: Double
        func area() -> Double {
            return side * side
        }
    }
    
    // Adding a new shape without modifying existing code
    struct Triangle: Shape {
        let base: Double
        let height: Double
        func area() -> Double {
            return 0.5 * base * height
        }
    }
    
    // Usage
    let shapes: [Shape] = [
        Circle(radius: 5),
        Square(side: 4),
        Triangle(base: 6, height: 3)
    ]
    
    for shape in shapes {
        print("Area: \(shape.area())")
    }

    In this example the Shape protocol defines a contract for all shapes. New shapes like CircleSquare, and Triangle can be added by conforming to the protocol without modifying existing code. This adheres to OCP by ensuring the system is open for extension (new shapes) but closed for modification (existing code remains unchanged).

    LSP-Liksov substitution principle

    The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented design, named after Barbara Liskov. It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a subclass should adhere to the contract established by its superclass, ensuring that it can be used interchangeably without causing unexpected behavior or violating the assumptions of the superclass. This principle emphasizes the importance of designing inheritance hierarchies carefully, ensuring that derived classes extend the base class’s functionality without altering its core behavior. Violations of LSP can lead to fragile code, bugs, and difficulties in maintaining and extending the system.

    protocol Vehicle {
        func move()
    }
    
    class Car: Vehicle {
        func move() {
            print("Car is moving")
        }
        
        func honk() {
            print("Honk honk!")
        }
    }
    
    class Bicycle: Vehicle {
        func move() {
            print("Bicycle is moving")
        }
        
        func ringBell() {
            print("Ring ring!")
        }
    }
    
    func startJourney(vehicle: Vehicle) {
        vehicle.move()
    }
    
    let car = Car()
    let bicycle = Bicycle()
    
    startJourney(vehicle: car)      // Output: Car is moving
    startJourney(vehicle: bicycle)  // Output: Bicycle is moving

    In this example, both Car and Bicycle conform to the Vehicle protocol, allowing them to be used interchangeably in the startJourney function without affecting the program’s behavior

    ISP-Interface segregation principle

    The Interface Segregation Principle (ISP) is one of the SOLID principles of object-oriented design, which states that no client should be forced to depend on methods it does not use. In other words, interfaces should be small, specific, and tailored to the needs of the classes that implement them, rather than being large and monolithic. By breaking down large interfaces into smaller, more focused ones, ISP ensures that classes only need to be aware of and implement the methods relevant to their functionality. This reduces unnecessary dependencies, minimizes the impact of changes, and promotes more modular and maintainable code. For example, instead of having a single interface with methods for printing, scanning, and faxing, ISP would suggest separate interfaces for each responsibility, allowing a printer class to implement only the printing-related methods.

    // Violating ISP
    protocol Worker {
        func work()
        func eat()
    }
    
    class Human: Worker {
        func work() {
            print("Human is working")
        }
        func eat() {
            print("Human is eating")
        }
    }
    
    class Robot: Worker {
        func work() {
            print("Robot is working")
        }
        func eat() {
            // Robots don't eat, but forced to implement this method
            fatalError("Robots don't eat!")
        }
    }
    
    // Following ISP
    protocol Workable {
        func work()
    }
    
    protocol Eatable {
        func eat()
    }
    
    class Human: Workable, Eatable {
        func work() {
            print("Human is working")
        }
        func eat() {
            print("Human is eating")
        }
    }
    
    class Robot: Workable {
        func work() {
            print("Robot is working")
        }
    }

    In this example, we first violate ISP by having a single Worker protocol that forces Robot to implement an unnecessary eat() method. Then, we follow ISP by splitting the protocol into Workable and Eatable, allowing Robot to only implement the relevant work() method.

    DIP-Dependency injection principle

    The Dependency Inversion Principle (DIP) is one of the SOLID principles of object-oriented design, which states that high-level modules (e.g., business logic) should not depend on low-level modules (e.g., database access or external services), but both should depend on abstractions (e.g., interfaces or abstract classes). Additionally, abstractions should not depend on details; rather, details should depend on abstractions. This principle promotes loose coupling by ensuring that systems rely on well-defined contracts (interfaces) rather than concrete implementations, making the code more modular, flexible, and easier to test or modify. For example, instead of a high-level class directly instantiating a low-level database class, it would depend on an interface, allowing the database implementation to be swapped or mocked without affecting the high-level logic.

    protocol Engine {
        func start()
    }
    
    class ElectricEngine: Engine {
        func start() {
            print("Electric engine starting")
        }
    }
    
    class Car {
        private let engine: Engine
        
        init(engine: Engine) {
            self.engine = engine
        }
        
        func startCar() {
            engine.start()
        }
    }
    
    let electricEngine = ElectricEngine()
    let car = Car(engine: electricEngine)
    car.startCar() // Output: Electric engine starting

    In this example, the Car class depends on the Engine protocol (abstraction) rather than a concrete implementation, adhering to the DIP

    Conclusions

    We have demonstrated how the SOLID principles align with Swift, so there is no excuse for not applying them. You can find source code used for writing this post in following repository

  • Copy-on-Write in Swift: Managing Value Types

    Copy-on-Write in Swift: Managing Value Types

    Copy on Write (CoW) is an interesting concept because it helps developers understand how Swift optimizes memory management by avoiding unnecessary data duplication. CoW ensures that objects are only copied when modified, saving resources and improving performance. This concept is particularly relevant when working with value types like structs or arrays, where multiple references can exist to the same data.

    By providing an example—such as modifying a struct inside an array—we aim to demonstrate how CoW prevents redundant copying, enhancing both efficiency and memory usage in iOS apps.

    CoW – Copy-on-Write

    Copy on Write (CoW) is an optimization technique used in Swift for value types, particularly for collections like Arrays, Strings, and Dictionaries. It allows these value types to share the same underlying storage until a mutation occurs, improving performance and memory efficiency.

    How Copy on Write Works

    1. Initial Assignment: When a value type is assigned to a new variable, Swift performs a shallow copy, creating a new reference to the same underlying data3.

    2. Shared Storage: Multiple instances of the value type share the same memory location, reducing unnecessary copying.

    3. Mutation Trigger: When one instance attempts to modify its contents, Swift creates a full copy of the data for that instance.

    4. Preserved Originals: The original instance remains unchanged, maintaining value semantics.

    Benefits

    • Performance Optimization: Avoids unnecessary copying of large data structures.

    • Memory Efficiency: Reduces memory usage by sharing data until modification is required.

    • Value Semantics: Maintains the expected behavior of value types while improving efficiency.

    Implementation

    CoW is built into Swift’s standard library for Arrays, Strings, and Dictionaries. For custom value types, developers can implement CoW behavior using techniques like:

    1. Using isUniquelyReferenced to check if a copy is needed before mutation.

    2. Wrapping the value in a reference type (e.g., a class) and managing copies manually.

    It’s important to note that CoW is not automatically available for custom value types and requires explicit implementation.

    Example code this time is provided via Playground:

    import UIKit
    
    final class Wrapper {
        var data: [Int]
        
        init(data: [Int]) {
            self.data = data
        }
    }
    
    struct CoWExample {
        private var storage: Wrapper
    
        init(data: [Int]) {
            self.storage = Wrapper(data: data)
        }
    
        mutating func modifyData() {
            print("Memory @ before updating: \(Unmanaged.passUnretained(storage).toOpaque())")
            
            if !isKnownUniquelyReferenced(&storage) {
                print("Making a copy of the data before modifying it.")
                storage = Wrapper(data: storage.data) // Created a copy
            } else {
                print("Update without copy, unique reference.")
            }
    
            storage.data.append(4)  // Modify array from class inside
            print("@ Memory after updaing: \(Unmanaged.passUnretained(storage).toOpaque())")
        }
    
        func printData(_ prefix: String) {
            print("\(prefix) Data: \(storage.data) | Memory @: \(Unmanaged.passUnretained(storage).toOpaque())")
        }
    }
    
    // Use  Copy-on-Write
    var obj1 = CoWExample(data: [1, 2, 3])
    var obj2 = obj1  // Both instances share same memory @
    
    print("Before updating obj2:")
    obj1.printData("obj1:")
    obj2.printData("obj2:")
    
    print("\nUpdating obj2:")
    obj2.modifyData() // Here will take place copy when there's a new reference
    
    print("\nAfter updating obj1:")
    obj1.printData("obj1:")
    obj2.printData("obj2:")

    Key Components:

    1. Wrapper Class:

      • final class that holds an array of integers (data).

      • It is used as the underlying storage for the CoWExample struct.

    2. CoWExample Struct:

      • Contains a private property storage of type Wrapper.

      • Implements the Copy-on-Write mechanism in the modifyData() method.

      • Provides a method printData(_:) to print the current data and memory address of the storage object.

    3. modifyData() Method:

      • Checks if the storage object is uniquely referenced using isKnownUniquelyReferenced(&storage).

      • If the storage object is not uniquely referenced (i.e., it is shared), a new copy of the Wrapper object is created before modifying the data.

      • If the storage object is uniquely referenced, the data is modified directly without creating a copy.

      • Appends the value 4 to the data array.

    4. Memory Address Tracking:

      • The memory address of the storage object is printed before and after modification to demonstrate whether a copy was made.

    Run playground

    Lets run the playground and analyze logs:

    After obj2=obj1 assignation, both share exactly same memory @ddress. But when obj2 is being updated:

    Is at that moment when obj2 is allocated in a different @dress space and then updated.

    Conclusions

    A quick answer is that the difference between Value Types and Reference Types lies in how they are assigned. When a Value Type is assigned, an isolated copy is created. However, in reality, after assignment, Value Types behave similarly to Reference Types. The key difference emerges when an update occurs—at that point, a new allocation is performed.

    You can find source code used for writing this post in following repository

    References

  • Enhancing iOS Apps with Redis

    Enhancing iOS Apps with Redis

    Connecting an iOS app to a Redis server can be highly beneficial, as it bridges the gap between mobile development and scalable backend solutions. Redis, known for its speed and efficiency, enhances performance by enabling fast caching, real-time data synchronization, and low-latency operations—critical for features like offline data storage, live notifications, and session management.

    In this post, we will build a Dockerized Node.js backend connected to a Dockerized Redis server. The client will be a simple iOS app that fetches a price list and updates the prices as well.

    Redis server

    The server is a sample Node.js application that will act as a backend facade. Let’s create a blank Node.js project:

    npm init -y

    To implement the server, we will need dotenv for loading environment variables from a .env file, express for setting up REST API endpoints, and ioredis for connecting to the Redis server.

    npm install dotenv express ioredis

    Node.Js server is following:

    require('dotenv').config(); // Fetch environment variables
    
    const express = require('express');
    const Redis = require('ioredis');
    
    const app = express();
    
    // Fetch environment variables
    const PORT = process.env.PORT || 3000;
    const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
    const REDIS_PORT = process.env.REDIS_PORT || 6379;
    
    // Connect with Redis by using environnment variables
    const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
    
    app.use(express.json());
    
    // 📌 Init some prices
    async function initializeSamplePrices() {
        const initialPrices = {
            "iphone": "999",
            "macbook": "1999",
            "ipad": "799",
            "airpods": "199"
        };
    
        for (const [product, price] of Object.entries(initialPrices)) {
            await redis.set(`price:${product}`, price);
        }
    
        console.log("✅ Prices initialized in Redis.");
    }
    
    // Initialize sample prices
    initializeSamplePrices();
    
    // 📌 Endpoint for getting a product price
    app.get('/price/:product', async (req, res) => {
        const price = await redis.get(`price:${req.params.product}`);
        
        if (price) {
            res.json({ product: req.params.product, price: price });
        } else {
            res.status(404).json({ error: "Product not found" });
        }
    });
    
    app.get('/prices', async (req, res) => {
        try {
            const keys = await redis.keys("price:*"); 
            
            // Fetch all prices
            const prices = await Promise.all(keys.map(async (key) => {
                const product = key.replace("price:", "");
                const price = await redis.get(key); 
                return { product: product, price: price };
            }));
    
            res.json(prices);
        } catch (error) {
            console.error("Error on getting prices:", error);
            res.status(500).json({ error: "Error on getting prices" });
        }
    });
    
    // 📌 Endpoint for adding or updating a product price
    app.post('/price', async (req, res) => {
        const { product, price } = req.body;
        if (!product || !price) {
            return res.status(400).json({ error: "Misssing data" });
        }
        
        await redis.set(`price:${product}`, price);
        res.json({ mensaje: "Price stored", product, price });
    });
    
    // 📌 Server listening on specied port
    app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
    

    This Node.js application sets up an Express server that interacts with a Redis database to store and retrieve product prices. It begins by loading environment variables using dotenv and establishes a connection to Redis with configurable host and port settings. When the server starts, it initializes Redis with some sample product prices (iPhone, MacBook, iPad, and AirPods). The Express app is configured to parse JSON requests, enabling it to handle API interactions effectively.

    The application provides multiple API endpoints: one for retrieving the price of a specific product (GET /price/:product), another for fetching all stored product prices (GET /prices), and a third for adding or updating a product price (POST /price). When a client requests a price, the server queries Redis, returning the value if found or a 404 error otherwise. The /prices endpoint retrieves all stored product prices by listing Redis keys and mapping them to their values. The /price POST endpoint allows clients to add or update prices by sending a JSON payload. Finally, the server listens on the configured port and logs a message indicating it is running.

    Sensitive intormateion is stored  in .env file:

    # Redis Configuration
    REDIS_HOST=redis
    REDIS_PORT=6379
    
    # Server Configuration
    PORT=3000

    Important: Please add .env to .gitignore to avoid compromising critical information in production environments. The next step is to define the Dockerfile for the Node.js server to ensure it is properly dockerized.

    FROM node:18
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["node", "server.js"]

    The Dockerfile sets up a containerized Node.js application. It begins with the official Node.js 18 image, sets the working directory to /app, and copies the package.json and package-lock.json files into the container. Then, it installs the dependencies using npm install. Afterward, it copies the rest of the application files into the container. The container exposes port 3000 and specifies node server.js as the command to run when the container starts, which typically launches the application server.

    To strictly copy only the necessary files into the container, create the following .dockerignore file:

    node_modules
    npm-debug.log
    .env
    .git
    

    The backend solution consists of two containerized servers. On one side, we have a Node.js backend facade that interacts with the iOS app. On the other side, the backend relies on cache storage in a Redis server.

    services:
      redis:
        image: redis:latest
        container_name: redis-server
        ports:
          - "6379:6379"
        restart: always
    
      backend:
        build: .
        container_name: node-backend
        ports:
          - "${PORT}:${PORT}"
        depends_on:
          - redis
        env_file:
          - .env

    This code defines a docker-compose.yml configuration file for setting up two services: a Redis server and a backend Node.js application. The Redis service uses the latest Redis image, exposing port 6379 and ensuring the container always restarts. The backend service is built from the current directory (.) using a Dockerfile, exposes a dynamic port based on the environment variable ${PORT}, and depends on the Redis service. Additionally, the backend container loads environment variables from a .env file. The depends_on directive ensures Redis starts before the backend service.

    Build image and deploy containers:

    docker-compose up --build -d

    Make sure both containers are running by using docker ps. Finally, type the following URL in any browser to fetch the prices: http://localhost:3000/prices.

    Backend side is ready!

    iOS Sample App

    The iOS sample app fetches product prices and allows users to update any price.

    Core app functionallity is following:

    import Foundation
    
    struct Product: Codable, Identifiable, Sendable {
        var id: String { product }
        let product: String
        var price: String
    }
    
    @MainActor
    class PriceService: ObservableObject {
        @Published var products: [Product] = []
    
        func fetchPrices() async {
            guard let url = URL(string: "http://localhost:3000/prices") else { return }
    
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                let decoder = JSONDecoder()
                if let productos = try? decoder.decode([Product].self, from: data) {
                    self.products = productos
                }
            } catch {
                print("Error fetching prices:", error)
            }
        }
    
        func updatePrice(product: String, price: String) async {
            guard let url = URL(string: "http://localhost:3000/price") else { return }
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
            let body: [String: String] = ["product": product, "price": price]
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
    
            do {
                let (_, _) = try await URLSession.shared.data(for: request)
                await self.fetchPrices()
            } catch {
                print("Error updating price: \(error.localizedDescription)")
            }
        }
    }

    This Swift code defines a model and a service for fetching and updating product prices. The Product struct represents a product with an ID, name, and price, conforming to the Codable, Identifiable, and Sendable protocols. The PriceService class is an observable object that manages a list of products and provides methods for interacting with a remote server.

    The fetchPrices() function asynchronously fetches product prices from a specified URL (http://localhost:3000/prices) using URLSession, decodes the returned JSON into an array of Product objects, and updates the products array. The updatePrice(product:price:) function sends a POST request to update the price of a specific product by sending a JSON payload to http://localhost:3000/price. After a successful update, it calls fetchPrices() again to refresh the list of products. Both functions handle errors by printing error messages if the network request fails.

    In this review, we update the price using the app and then return to the browser to verify that the price update has been reflected in the Redis server.

    Conclusions

    In this project, we demonstrated how easy it is to set up a sample backend server using Redis and interact with it through an iOS app as the client.

    You can find source code used for writing this post in following repository

    References

  • Opaque types in Swift

    Opaque types in Swift

    Opaque types in Swift introduces developers to a powerful feature that enhances code abstraction, flexibility, and performance. By using the some keyword, opaque types allow developers to hide implementation details while maintaining type safety, unlike other techniques like Any or AnyObject.

    The aim of thi post is clarify their usage in real-world scenarios, help bridge the knowledge gap for developers who are familiar with protocols and generics but not opaque types, and showcase how this feature leads to cleaner, more maintainable code.

    Opaque types

    Opaque types in Swift, introduced with the some keyword, allow you to define a type that hides its concrete implementation while still conforming to a specific protocol or set of constraints. This enables you to return a value from a function without exposing the exact type, providing flexibility and abstraction while maintaining type safety. Essentially, the compiler knows the specific type behind the opaque type, but users of the type only know that it conforms to the expected protocol, which helps in writing cleaner, more maintainable code without sacrificing performance or type safety.

    Lets start by a generic sample:

    protocol Animal {
        func makeSound() -> String
    }
    
    struct Dog: Animal {
        func makeSound() -> String { "Woof!" }
    }
    
    struct Cat: Animal {
        func makeSound() -> String { "Meow!" }
    }
    
    func getAnimal() -> some Animal {
        return Dog()
    }

    In this example:

    • The getAnimal() function returns an opaque type (some Animal), indicating that it returns a type conforming to the Animal protocol.

    • The specific type (Dog) is hidden from the caller.

    Benefits:
    1. Abstraction: Hides implementation details while exposing only necessary behavior.

    2. Type Safety: Ensures that the returned type is consistent and conforms to the specified protocol.

    3. Optimization: Allows the compiler to optimize code by knowing the exact underlying type.

    Now let me introduce you another example that you most probably have seen in swiftUI:

    struct ContentView: View {
        var body: some View {
            VStack {
                myCustomSomeViewA()
                myCustomSomeViewB()
            }
        }
        
        func myCustomSomeViewA() -> some View {
            Text("myCustomSomeViewA")
         }
        
        func myCustomSomeViewB() -> some View {
            Text("myCustomSomeViewB")
         }
    }

    The some keyword in this SwiftUI code is used to specify opaque return types for the body property and the helper functions myCustomSomeViewA() and myCustomSomeViewB(), which return SwiftUI views without exposing their exact types. This allows the compiler to enforce type consistency while keeping implementation details hidden. In practice, it ensures that each function returns a single, consistent view type, improving type safety and optimization.

    any keyword

    In Swift, the any keyword is used to explicitly indicate existential types, meaning a value can be of any type that conforms to a given protocol. Introduced in Swift 5.6, it improves clarity by distinguishing between existentials (any Protocol) and generics (<T: Protocol>), helping developers understand when dynamic dispatch and runtime type checking are involved. Using any makes code more explicit but may introduce performance overhead compared to generics, which enable static dispatch. It is recommended when working with heterogeneous types but should be avoided when generics provide a more efficient alternative.

    We will continue with previous generic example:

    func printAnimal(_ animal: any Animal) -> String {  // Explicit existential
        animal.makeSound()
    }

    The function printAnimal(_ animal: any Animal) -> String takes an existential any Animal as a parameter and calls its makeSound() method but has a compilation error because it declares a return type of String without returning a value. To fix it, makeSound() should return a String, and the function should return that value. For example, if Animal is a protocol with func makeSound() -> String, and a Dog struct implements it by returning "Woof!", calling printAnimal(Dog()) would correctly return "Woof!".

    On extending previous SwiftUI example with any:

    The error …this expreession cannot conform to ‘View’ occurs because the any keyword creates an existential type, which cannot directly conform to the View protocol in SwiftUI. This is a common issue when working with protocols and generic types in SwiftUI.

    For fixing this issue we have to address in the following way:

        let views: [AnyView] = [AnyView(myCustomAnyViewA()),
                                AnyView(myCustomAnyViewB())]

    AnyView in SwiftUI is a type-erased wrapper that allows for changing the type of view used in a given view hierarchy13. It serves as a container that can hold any type of SwiftUI view, enabling developers to return multiple view types from a single function or computed property4.

    Conclusions

    In this post, I clarify the concept of Opaque Types in Swift and the usage of the some and any keywords. You can find source code used for writing this post in following repository

    References

  • Swift Package Manager Simplified

    Swift Package Manager Simplified

    The Swift Package Manager (SPM) helps developers modularize code, improve reusability, and streamline dependency management using Apple’s preferred tool. Many iOS developers are still transitioning from CocoaPods and Carthage, making a clear guide on creating and integrating SPM packages highly relevant. Additionally, SPM encourages open-source contributions, enhances team collaboration, and improves build times by promoting a more structured development approach.

    In this post, we will implement a simple SPM package that generates a random dice value. Later, we will integrate it into an iOS app that displays the dice value.

    Dice SPM

    The first step is to create and navigate into the folder that will contain the SPM package implementation. Use the following command to create the library scaffolding.

    swift package init --type library

    Open project with XCode.

    xed . 

    This is folder structure for the project:

    This is what it does our SPM:

    public struct DiceSPM {
        public static func roll() -> String {
            let values = ["Ace", "J", "K", "Q", "Red", "Black"]
            return values[Int.random(in: 0...(values.count - 1))]
        }
    }

    And its tests:

    @Test func example() async throws {
        var dicValues: [String: Int] = ["Ace": 0, "J": 0, "K": 0, "Q": 0, "Red": 0, "Black": 0]
        for _ in 0..<100 {
            let result: String = DiceSPM.roll()
            dicValues[result]! += 1
        }
        
        for value in dicValues.keys {
            #expect(dicValues[value] ?? 0 > 0)
        }
    }

    Build and run tests:

    Create an new GitHub public repository and upload all generated stuff:

    Last but not least, documenting the README.md file is always a good practice for regular source code, but for libraries (such as SPMs), it is a MUST.

    You can find SPM hosted in following GitHub repository.

    Dice Consumer

    DiceConsumer will be a simple app that retrieves values from the DiceSPM package. The first step is to import the SPM package.

    And just call SPM library implementation from View:

    import SwiftUI
    import DiceSPM
    
    struct ContentView: View {
        @State private var dice: String?
        var body: some View {
            VStack {
                if let dice = dice {
                    Text(dice)
                        .font(.largeTitle)
                }
                Button {
                    dice = DiceSPM.roll()
                } label: {
                    Text("Roll the dice!")
                }
    
            }
            .padding()
        }

    Finally build and deploy on simulator:

    Conclusions

    CocoaPods is no longer maintained, and Swift Package Manager (SPM) was intended to replace it and has now successfully succeeded it. In this post, I have demonstrated how easy it is to publish an SPM package and import it into any project.

    You can find source code used for writing this post in following repository

  • Breaking Retain Cycles in Swift

    Breaking Retain Cycles in Swift

    Detecting and preventing retain cycles is crucial, as they lead to memory leaks, degrade app performance, and cause unexpected behaviors. Many developers, especially those new to Swift and UIKit, struggle with understanding strong reference cycles in closures, delegates, and class relationships.

    We will present two classic retain cycle bugs in a sample iOS app, explore the tools that Xcode provides for detecting them, and share some advice on how to avoid them.

    Memory Graph Debuger

    The sample application consists of two view screens. The pushed screen contains injected retain cycles, leading to memory leaks. A memory leak occurs when memory references cannot be deallocated. In this app, the leak happens when the pushed screen is popped back but remains in memory.

    Build and deploy app on simulator (or real device): 

    Open Memory Graph Debuger

    In this case is clear where do we have a retain cycle.

    class MyViewModel: ObservableObject {
        @Published var count: Int = 0
        var classA: ClassA  = ClassA()
        
        var incrementClosure: (() -> Void)?
        
        init() {
    ...
            
            #if true
            incrementClosure = {
                self.count += 1
            }
            #else
    ...
            }
            #endif
        }
        
        deinit {
            print("MyViewModel is being deallocated")
        }
    }
    
    struct SecondView: View {
        @StateObject private var viewModel = MyViewModel()
        var body: some View {

    In SecondView, MyViewModel is referenced using viewModel, MyViewModel.incrementalClosure, and self, which also references MyViewModel indirectly. When the view is popped, this class cannot be removed from memory because it is retained due to an internal reference from self.count.

    If you set a breakpoint in the deinit method, you will notice that it is never triggered. This indicates that the class is still retained, leading to a memory leak. As a result, the memory allocated for MyViewModel will never be deallocated or reused, reducing the available memory for the app. When the app runs out of memory, iOS will forcefully terminate it.

    The only way to break this retain cycle is to make one of these references weak. Using a weak reference ensures that it is not counted toward the retain count. When the view is popped, SecondView holds the only strong reference, allowing iOS to deallocate MyViewModel and free up memory.

    This is the correct solution:

    class MyViewModel: ObservableObject {
        @Published var count: Int = 0
        var classA: ClassA  = ClassA()
        
        var incrementClosure: (() -> Void)?
        
        init() {
            ...
            
            #if false
          ....
            #else
            incrementClosure = { [weak self] in
                self?.count += 1
            }
            #endif
        }
        
        deinit {
            print("MyViewModel is being deallocated")
        }
    }

    Set a breakpoint in deinit to verify that the debugger stops when the view is popped. This confirms that the class has been properly deallocated

    Next retain cycle is a memory reference cycle, when we have a chain of refenced classes and once of them is referencing back it generates a loop of references. For implementing this memory leak we have created a classA that references a classB that references a classC that finally refences back to classA.

    Here we can see clear that same memory address is referenced. But if we take a look at Debug Memory Inspector

    It is not as clear as the previous case. This is a prepared sample app, but in a real-world application, the graph could become messy and make detecting memory leaks very difficult. Worst of all, with this kind of memory leak, when the view is removed, the deinit method is still being executed.

    For detecting such situations we will have to deal with another tool.

    Insruments

    Xcode Instruments is a powerful performance analysis and debugging tool provided by Apple for developers to profile and optimize their iOS, macOS, watchOS, and tvOS applications. It offers a suite of tools that allow developers to track memory usage, CPU performance, disk activity, network usage, and other system metrics in real-time. Instruments work by collecting data through time-based or event-based profiling, helping identify performance bottlenecks, memory leaks, and excessive resource consumption. Integrated within Xcode, it provides visual timelines, graphs, and detailed reports, making it an essential tool for fine-tuning app efficiency and responsiveness.

    In XCode Product menu select Profile:

    For measuring memory leaks select ‘Leaks»:

    Press record button for deploying on simulator and start recording traces.

    In following video, you will see that when view is pushed back then memory leak is detected:

    Is programed  to check memory every 10 seconds, when we click on red cross mark then bottom area shows the classes affected:

    Conclusions

    In this post, I have demonstrated how to detect memory leaks using the Memory Graph Debugger and Inspector. However, in my opinion, preventing memory leaks through good coding practices is even more important than detecting them.

    In Swift, memory leaks typically occur due to retain cycles, especially when using closures and strong references. To avoid memory leaks, you can use weak references where appropriate.

    You can find source code used for writing this post in following repository

    References

  • Boost Security: Enable Touch ID & Face ID

    Boost Security: Enable Touch ID & Face ID

    With the increasing reliance on biometric authentication for secure and seamless access, developers must understand how to integrate these features effectively. By breaking down the process, this post empowers developers to build more secure and user-friendly applications, aligning with Apple’s emphasis on privacy and cutting-edge technology.

    In this micro-post, you’ll see just how easy it is to implement biometric authentication on iOS.

    Authentication App

    Before start codign we need to fill in a message on ‘FaceIDUssageAuthentication:

    This is the view:

    struct ContentView: View {
        @State private var isAuthenticated = false
        @State private var errorMessage = ""
    
        var body: some View {
            VStack {
                if isAuthenticated {
                    Text("Authentication successful!")
                        .font(.title)
                        .foregroundColor(.green)
                } else {
                    Text(errorMessage)
                        .font(.title)
                        .foregroundColor(.red)
                }
    
                Button(action: {
                    authenticate()
                }) {
                    Text("Authenticate with Touch ID / Face ID")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }
            .padding()
        }

    This code creates a simple SwiftUI view for handling biometric authentication. It displays a success or error message based on the authentication status and provides a button to trigger the authentication process. 

    And this is the authentication code:

        func authenticate() {
            let context = LAContext()
            var error: NSError?
    
            if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
                let reason = "Authenticate for having access to application"
    
                context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
                    DispatchQueue.main.async {
                        if success {
                            isAuthenticated = true
                            errorMessage = ""
                        } else {
                            isAuthenticated = false
                            errorMessage = "Failed authentication"
                        }
                    }
                }
            } else {
                isAuthenticated = false
                errorMessage = "Touch ID / Face ID no está disponible"
            }
        }

    The authenticate() function uses the Local Authentication framework to enable biometric authentication (Touch ID or Face ID) for accessing an application. It first checks if the device supports biometric authentication using canEvaluatePolicy. If supported, it prompts the user to authenticate with a reason message, and upon success, sets isAuthenticated to true and clears any error messages; if authentication fails, it sets isAuthenticated to false and updates errorMessage to indicate the failure. If biometric authentication is unavailable or not configured, it sets isAuthenticated to false and updates errorMessage to reflect that Touch ID/Face ID is not available. The function ensures UI updates are performed on the main thread, making it suitable for integration into apps requiring secure user access.

    Finally deploy in a real device:

    Conclusions

    As you can see in the code above, it is easy to integrate the same biometric authentication mechanism used to unlock the iPhone into your apps.

    You can find source code used for writing this post in following repository

    References

  • Boosting iOS App Flexibility with Firebase Remote Config

    Boosting iOS App Flexibility with Firebase Remote Config

    This post demonstrates how developers can dynamically update app behavior, UI elements, and features without requiring an App Store update. This capability is especially valuable for A/B testing, feature flagging, and personalized user experiences, making apps more adaptable and data-driven. Despite its benefits, many developers underutilize Remote Config. A step-by-step guide covering Firebase setup, SDK integration, fetching configurations, and best practices would offer significant value.

    In this post, we’ll walk through creating a sample iOS app that reads a feature flag and, based on its value, displays a different UI message.

    Setup Firebase and XCode

    In a previous post, Unlocking Firebase iOS Push Notifications, we explained how to set up a Firebase project for iOS from scratch. During this process, you obtained a file called GoogleService-Info.plist.

    You have to include it on your project, next step is include SPM Firebase Library.

    Add FirebaseCore and FirebaseRemoteConfig to your target.

    Create a Firebase Configuration Flag

    From your Firebase project side bar:

    Select ‘Remote Config’. And add a new parameter:

    In our implementation, ‘NewTentativeFeatureFlag’ will be a boolean (false) parameter. Once set, ensure that it is properly published.

    The iOS Remote Configuration App

    The Main ContentView retrieves the newTentativeFeatureFlag @Published attribute from the RemoteConfigManager component in Firebase:

    struct ContentView: View {
        @State private var showNewFeature = false
        @StateObject var remoteConfigManager = appSingletons.remoteConfigManager
        var body: some View {
            VStack {
                if remoteConfigManager.newTentativeFeatureFlag {
                    Text("New Feature is Enabled!")
                        .font(.largeTitle)
                        .foregroundColor(.green)
                } else {
                    Text("New Feature is Disabled.")
                        .font(.largeTitle)
                        .foregroundColor(.red)
                }
            }
        }
    }

    Depending on the value obtained, a different text is printed with a different color. This is a very simplistic example, but it could represent an A/B test, a new feature, or any functionality you want to run live.

    An alternative valid implementation could be to call getBoolValue inside the .onAppear modifier instead of using the @published attribute from RemoteConfigManager.

    RemoteConfigManager is the component that wraps Firebase Remote Config functionality.

    import Foundation
    import Firebase
    
    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    @GlobalManager
    class RemoteConfigManager: ObservableObject {
        @MainActor
        @Published var newTentativeFeatureFlag: Bool = false
        
        private var internalNewTentativeFeatureFlag = false {
            didSet {
                Task { @MainActor [internalNewTentativeFeatureFlag]  in
                    newTentativeFeatureFlag = internalNewTentativeFeatureFlag
                }
            }
        }
        
        private var remoteConfig: RemoteConfig =  RemoteConfig.remoteConfig()
        private var configured = false
    
        @MainActor
        init() {
            Task { @GlobalManager in
                await self.setupRemoteConfig()
            }
        }
        
        private func setupRemoteConfig() async {
            guard !configured else { return }
            
            let settings = RemoteConfigSettings()
            settings.minimumFetchInterval = 0
            remoteConfig.configSettings = settings
            
            fetchConfig { [weak self] result in
                guard let self else { return }
                Task { @GlobalManager [result] in
                    configured = result
                    self.internalNewTentativeFeatureFlag = self.getBoolValue(forKey: "NewTentativeFeatureFlag")
                }
            }
        }
    
        private func fetchConfig(completion: @escaping @Sendable (Bool) -> Void) {
            remoteConfig.fetch { status, error in
                if status == .success {
                    Task { @GlobalManager in
                        self.remoteConfig.activate { changed, error in
                            completion(true)
                        }
                    }
                } else {
                    completion(false)
                }
            }
        }
        
        func getBoolValue(forKey key: String) -> Bool {
            return remoteConfig[key].boolValue
        }
        
        func getStringValue(forKey key: String) -> String {
            return remoteConfig[key].stringValue
        }
    }

    The code is already compatible with Swift 6 and defines a RemoteConfigManager class responsible for fetching and managing Firebase Remote Config values in a SwiftUI application. To ensure thread safety, all operations related to Remote Config are handled within a global actor (GlobalManager).

    The RemoteConfigManager class conforms to ObservableObject, allowing SwiftUI views to react to changes in its properties. The newTentativeFeatureFlag property is marked with @Published and updated safely on the main actor to maintain UI responsiveness.

    The class initializes Remote Config settings, fetches values asynchronously, and updates internalNewTentativeFeatureFlag accordingly. This design ensures efficient Remote Config value retrieval while maintaining proper concurrency handling in a Swift application.

    Build and run

    As title suggests, build and run:

    In Firebase Remote Config, ‘NewTentativeFeatureFlag’ is set to true, and the view is displaying the correct message. Now, let’s switch the value to false and restart the application. Yes, I said restart, because when the value is changed in the console, Firebase Remote Config has no mechanism to notify the app. The app must periodically fetch the value to detect any changes in its state.

    Now turn ‘NewTentativeFeatureFlag’  to false and re-start the app.

    Conclusions

    Firebase Remote Config is essential for implementing A/B tests and serves as a great mechanism for disabling immature functionalities that might fail in production (e.g., a new payment method).

    You can find source code used for writing this post in following repository

    References

  • Boost Your iOS Development with Preprocessing Directives

    Boost Your iOS Development with Preprocessing Directives

    Preprocessor directives such as #if, #else, #endif, and #define are powerful tools in Objective-C and Swift. They enable developers to conditionally compile code, manage different build configurations, and optimize apps for various platforms or environments. Understanding these concepts helps streamline code, improve debugging, and create more flexible, maintainable projects.
    In the following post, I will present code snippets that demonstrate the use of preprocessing directives. I hope you find them useful!

    Preprocessing directives

    Preprocessing directives in Swift are instructions that are interpreted by the Swift compiler before the actual compilation of the code begins. These directives allow developers to include or exclude portions of code based on certain conditions, such as the target operating system, compiler flags, or custom-defined conditions. Common preprocessing directives in Swift include #if#elseif#else, and #endif, which are used for conditional compilation. For example, you can use these directives to write platform-specific code, ensuring that only the relevant code for a particular platform (like iOS or macOS) is compiled. This helps in maintaining a single codebase for multiple platforms while still accommodating platform-specific requirements.

    The concept of preprocessing directives originates from the C programming language, where the C preprocessor (cpp) is used to manipulate the source code before it is compiled. Swift, being influenced by C and Objective-C, adopted a similar mechanism but with some differences. Unlike C, Swift does not have a separate preprocessor; instead, these directives are handled directly by the Swift compiler. This integration simplifies the build process and avoids some of the complexities and pitfalls associated with traditional preprocessors. The use of preprocessing directives in Swift reflects the language’s goal of providing modern, safe, and efficient tools for developers while maintaining compatibility with existing practices from its predecessor languages.

    #if, #elsif, #else and #endif

    Platform-Specific Code

    You can use #if to write code that compiles only for specific platforms, such as iOS, macOS, or Linux. This is useful for handling platform-specific APIs or behavior.

    #if os(iOS)
        print("Running on iOS")
    #elseif os(macOS)
        print("Running on macOS")
    #elseif os(Linux)
        print("Running on Linux")
    #else
        print("Running on an unknown platform")
    #endif

    Custom Compiler Flags

    Also you can define custom compiler flags in your build settings and use them to conditionally compile code. For example, you might want to include debug-only code or feature toggles.

    #if DEBUG
        print("Debug mode is enabled")
    #elseif RELEASE
        print("Release mode is enabled")
    #else
        print("Unknown build configuration")
    #endif

    Feature Toggles

    Use conditional compilation to enable or disable features based on custom conditions.

            #if EXPERIMENTAL_FEATURE
                print("Experimental feature is enabled")
            #else
                print("Experimental feature is disabled")
            #endif
     

    Open the target settings and add -DEXPERIMENTAL_FEATURE on Other Swift Flags

    Checking Swift Version

    You can use conditional compilation to check the Swift version being used.

    #if swift(>=5.0)
        print("Using Swift 5.0 or later")
    #else
        print("Using an older version of Swift")
    #endif
     

    #available

    The #available directive in Swift is used to check the availability of APIs at runtime based on the operating system version or platform. This is particularly useful when you want to use newer APIs while maintaining compatibility with older versions of the operating system.

    Purpose of #available is ensure that your app does not crash when running on older versions of the operating system that do not support certain APIs. Also it allows you to provide fallback behavior for older OS versions.

    if #available(iOS 15, macOS 12.0, *) {
        // Use APIs available on iOS 15 and macOS 12.0 or later
        let sharedFeature = SharedFeatureClass()
        sharedFeature.doSomething()
    } else {
        // Fallback for older versions
        print("This feature requires iOS 15 or macOS 12.0.")
    }

    If your code runs on multiple platforms (e.g., iOS and macOS), you can check availability for each platform.

    #warning and #error:

    The #warning and #error directives in Swift are used to emit custom warnings or errors during compilation. These are helpful for:

    1. Marking incomplete or problematic code.

    2. Enforcing coding standards or requirements.

    3. Providing reminders for future work.

    4. Preventing compilation if certain conditions are not met.

    Unlike runtime warnings or errors, these directives are evaluated at compile time, meaning they can catch issues before the code is even run.

    The #warning directive generates a compile-time warning. It does not stop the compilation process but alerts the developer to potential issues or tasks that need attention.

    func fetchData() {
        #warning("Replace with network call once API is ready")
        let data = mockData()
    }

    The #error directive generates a compile-time error. It stops the compilation process entirely, ensuring that the code cannot be built until the issue is resolved.

    func newFeature() {
        #error("This feature is not yet implemented.")
    }

    Conclusions

    In this post, I have provide you a few preprocessiong directives that I have considered most usefull. You can find source code used for writing this post in following repository