Categoría: Swift

  • Seamless Keychain Data Migrations in iOS

    Seamless Keychain Data Migrations in iOS

    It’s a common but poorly documented challenge that many developers face, especially during device upgrades, app reinstallations, or when sharing data across app groups. Since the Keychain stores sensitive user information like credentials and tokens, handling its migration securely is critical for maintaining a seamless user experience and ensuring data integrity.

    This post explains how to implement a trial period for users in an iOS app. To prevent users from restarting the trial period by reinstalling the app, the controlling information should be securely stored in the Keychain.

    Keychain

    Keychain in iOS is a secure storage system provided by Apple that allows apps to store sensitive information such as passwords, cryptographic keys, and certificates securely. It uses strong encryption and is protected by the device’s hardware and the user’s authentication (e.g., Face ID, Touch ID, or passcode). The Keychain ensures that this data is safely stored and only accessible to the app that created it, unless explicitly shared through keychain access groups. It provides a convenient and secure way for developers to manage credentials and other private data without implementing their own encryption systems.

    When an app is removed (uninstalled) from an iOS device, most of its data—including files stored in its sandboxed file system—is deleted. However, data stored in the Keychain is not automatically deleted when the app is removed. This means that if the app is reinstalled later, it can still access its previously stored Keychain data (assuming the Keychain item was saved with the correct accessibility settings and not tied to a now-invalid access group). This behavior allows for features like remembering a user’s login after reinstalling an app.

    Persisted local data can change over the lifetime of an application, and apps distributed in the past may not always be updated to the latest version. To handle this, apps must implement a data migration mechanism to adapt old stored data to the new data model. Without proper migration, the app may crash when attempting to load outdated or incompatible data. When data is stored in UserDefaults, this issue can often be bypassed by simply reinstalling the app—since UserDefaults is part of the app’s sandbox and gets cleared upon uninstallation, the app starts fresh, and the user can continue using it. However, Keychain is not part of the app bundle; it is managed by the iOS operating system and persists even after the app is uninstalled. Therefore, if the app crashes while trying to parse outdated or incompatible data from the Keychain, it will continue to crash even after reinstallation. In such cases, the app developer will most likely need to release a new version with the necessary fixes.

    Sample App

    In this post, we will implement a sample app that includes a trial mechanism. This means the app will be freely usable for a limited period. After that, users will need to complete certain tasks to unlock continued usage. It’s highly likely you’ve encountered similar applications before.

    The data structure that controls the trial mechanism can only be stored in the keychain for two main reasons. First, it requires a secure storage location. Second—and equally important—it must be stored in a place that retains the information even if the app is uninstalled. Otherwise, users could simply reinstall the app to reset the trial period and continue using it without restriction.

    The structure that controls trial mechanism is following:

    typealias TrialInfoLatestModel = TrialInfo
    
    protocol TrialInfoMigratable:Codable {
        var version: Int { get }
        func migrate() -> TrialInfoMigratable?
    }
    
    // Current model (v0)
    struct TrialInfo: Codable, TrialInfoMigratable, Equatable {
        var version: Int = 0
        let startDate: Date
        
        static let defaultValue = TrialInfo(startDate: Date())
        
        func migrate() -> (any TrialInfoMigratable)? {
            nil
        }
    }

    This Swift code defines a versioned data model system for TrialInfo, where TrialInfoMigratable is a protocol that allows models to specify their version and migrate to newer versions. The TrialInfo struct represents version 0 of the model, contains a startDate, and conforms to Codable, Equatable, and TrialInfoMigratable. It includes a static default value and a migrate() method that returns nil, indicating no further migration is needed. The typealias TrialInfoLatestModel = TrialInfo serves as an alias for the latest version of the model, making future upgrades easier by allowing seamless substitution with newer model versions.

    Next is review the function that handles migration placed on viewModel:

    @Suite("TrialViewModelTest", .serialized) // Serialize for avoiding concurrent access to Keychain
    struct TrialViewModelTest {
    
        @Test("loadTrialInfo when nil")
        func loadTrialInfoWhenNil() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            // When
            let info = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(info == nil)
        }
       
        @Test("Load LatestTrialInfo when previous stored TrialInfo V0")
        func loadTrialInfoWhenV0() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfo(startDate: Date.now)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 0)
        }
    }

    Basically validate then trial data has not been and has been stored. Once, we are sure that tests pass then deploy the app into the simulator.

    review

    First change on Trial Data

    The core idea behind a migration mechanism is to keep incoming changes as simple as possible.
    We now propose updating the trial data structure with two new attributes (v1).
    Installing the app for the first time with v1 will not pose any problems. However, issues may arise when the app was initially installed with version 0 (v0) and later updated to v1.
    In such cases, the app must perform a migration from v0 to v1 upon startup.

    The following changes will be made to the Trial data structure:

    typealias TrialInfoLatestModel = TrialInfoV1
    
    .....
    
    // Current model (v1)
    struct TrialInfoV1: Codable, TrialInfoMigratable {
        var version: Int = 1
        let startDate: Date
        let deviceId: String
        let userId: String
        
        static let defaultValue = TrialInfoV1(startDate: Date(), deviceId: UUID().uuidString, userId: UUID().uuidString)
        
        init(startDate: Date, deviceId: String, userId: String) {
            self.startDate = startDate
            self.deviceId = deviceId
            self.userId = userId
        }
        
        func migrate() -> (any TrialInfoMigratable)? {
            nil
        }
    }
    
    // Current model (v0)
    struct TrialInfo: Codable, TrialInfoMigratable, Equatable {
        ...
        
        func migrate() -> (any TrialInfoMigratable)? {
            TrialInfoV1(startDate: self.startDate, deviceId: UUID().uuidString, userId: UUID().uuidString)
        }
    }

    Typealias has to be set to latest version type and we have to implement the migration function that converts v0 to v1. And in the migration new type also to migration function:

        func loadTrialInfo(key: String) async -> TrialInfoLatestModel? {
            ...
            let versionedTypes: [TrialInfoMigratable.Type] = [
                TrialInfo.self
            ]
            ...
        }

    No more migration changes, no execute unit tests:

    Screenshot

    Unit tests fails mainly because v0 stored was migrated to v1. Adapt unit test and new following test:

    @Suite("TrialViewModelTest", .serialized) // Serialize for avoiding concurrent access to Keychain
    struct TrialViewModelTest {
    ...
       
        @Test("Load LatestTrialInfo when previous stored TrialInfo V0")
        func loadTrialInfoWhenV0() async throws {
           ....
            // Then
            #expect(trialInfoStored?.version == 1)
        }
        
        @Test("Load LatestTrialInfo when previous stored TrialInfo V1")
        func loadTrialInfoWhenV1() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfoV1(startDate: Date.now, deviceId: UUID().uuidString, userId: UUID().uuidString)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 1)
        }
    }

    Repeat test execution to ensure everything is operating safely and as expected.

    Next trial data

    Next change, v2, removes one of the attributes added in v1. Changes on trial data structure are following:

    typealias TrialInfoLatestModel = TrialInfoV2
    
    ...
    // Current model (v2)
    struct TrialInfoV2: Codable, TrialInfoMigratable {
        
        var version: Int = 2
        let startDate: Date
        let deviceId: String
        
        static let defaultValue = TrialInfoV2(startDate: Date(), deviceId: UUID().uuidString)
        
        init(startDate: Date, deviceId: String) {
            self.startDate = startDate
            self.deviceId = deviceId
        }
        
        func migrate() -> (any TrialInfoMigratable)? {
           nil
        }
    }
    
    // Current model (v1)
    struct TrialInfoV1: Codable, TrialInfoMigratable {
        ...
        func migrate() -> (any TrialInfoMigratable)? {
            TrialInfoV2(startDate: self.startDate, deviceId: self.deviceId)
        }
    }
    ...
    }
    
    
    
    

    Shift typealias to latest defined type and implement the migration function that transform v1 to v2. Adapt also viewmodel migration function and add v1 type to type array:

            let versionedTypes: [TrialInfoMigratable.Type] = [
                TrialInfoV1.self,
                TrialInfo.self
            ]

    Finally run the test, adapt them and add test case for v2:

        @Test("Load LatestTrialInfo when previous stored TrialInfo V2")
        func loadTrialInfoWhenV2() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfoV2(startDate: Date.now, deviceId: UUID().uuidString)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 2)
        }

    Conclusions

    In this post, I presented a common issue encountered when working with persisted data, along with a possible solution for handling data migration.

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

    References

  • Custom @propertyWrapper in Action

    Custom @propertyWrapper in Action

    @propertyWrapper is interesting because it demystifies an advanced Swift feature that helps encapsulate logic, reduce boilerplate, and improve code maintainability. Many developers may not fully utilize property wrappers, despite their practical applications in areas like UserDefaults management, data validation, and SwiftUI state handling (@State, @Published, etc.). By providing clear explanations, real-world examples, and best practices we will present a pair of examples where it could be interesting approach implementation by using @properyWrapper.

    Custom @propertyWrapper

    A custom property wrapper in Swift is a specialized type that allows you to add custom behavior or logic to properties without cluttering the main class or struct code. It’s a powerful feature introduced in Swift 5 that enables developers to encapsulate common property-related functionality, such as validation, transformation, or persistence, in a reusable manner.

    Custom property wrappers are particularly useful for:

    1. Encapsulating repetitive code patterns.

    2. Adding validation or transformation logic to properties1.

    3. Implementing persistence mechanisms, such as UserDefaults storage.

    4. Creating SwiftUI-compatible state management solutions.

    Clamped type example

    First example is a clamped type, that means an int ranged value:

    @propertyWrapper
    struct Clamped<Value: Comparable> {
        private var value: Value
        let range: ClosedRange<Value>
    
        init(wrappedValue: Value, _ range: ClosedRange<Value>) {
            self.range = range
            self.value = range.contains(wrappedValue) ? wrappedValue : range.lowerBound
        }
    
        var wrappedValue: Value {
            get { value }
            set { value = min(max(newValue, range.lowerBound), range.upperBound) }
        }
    }
    
    // Uso del Property Wrapper
    struct Player {
        @Clamped(wrappedValue: 50, 0...100) var health: Int
    }
    
    var player = Player()
    player.health = 120
    print(player.health) // Output: 100 (se ajusta al máximo del rango)

    The Clamped property wrapper ensures that a property’s value remains within a specified range. It takes a ClosedRange<Value> as a parameter and clamps the assigned value to stay within the defined bounds. When a new value is set, it uses min(max(newValue, range.lowerBound), range.upperBound) to ensure the value does not go below the lower bound or exceed the upper bound. If the initial value is outside the range, it is automatically set to the lower bound. This makes Clamped useful for maintaining constraints on variables that should not exceed predefined limits.

    In the Player struct, the health property is wrapped with @Clamped(wrappedValue: 50, 0...100), meaning its value will always stay between 0 and 100. If we set player.health = 120, it gets clamped to 100 because 120 exceeds the maximum allowed value. When printed, player.health outputs 100, demonstrating that the wrapper effectively enforces the constraints. This approach is particularly useful in scenarios like game development (e.g., keeping player health within a valid range) or UI elements (e.g., ensuring opacity remains between 0.0 and 1.0).

    UserDefaults example

    Second example is a UserDefaults sample:

    @propertyWrapper
    struct UserDefault<T> {
        let key: String
        let defaultValue: T
    
        var wrappedValue: T {
            get {
                return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
            }
            set {
                UserDefaults.standard.set(newValue, forKey: key)
            }
        }
    }
    
    @MainActor
    struct Settings {
        @UserDefault(key: "username", defaultValue: "Guest")
        static var username: String
    }
    
    Settings.username = "SwiftUser"
    print(Settings.username) // Output: "SwiftUser"

    This code defines a property wrapper, UserDefault<T>, which allows easy interaction with UserDefaults in Swift. The wrapper takes a generic type T, a key for storage, and a defaultValue to return if no value is found in UserDefaults. The wrappedValue property is used to get and set values in UserDefaults: when getting, it retrieves the stored value (if available) or falls back to the default; when setting, it updates UserDefaults with the new value.

    The Settings struct defines a static property username using the @UserDefault wrapper. This means Settings.username reads from and writes to UserDefaults under the key "username". When Settings.username = "SwiftUser" is set, the value is stored in UserDefaults. The subsequent print(Settings.username) retrieves and prints "SwiftUser" since it was saved, demonstrating persistent storage across app launches.

    Conclusions

    By using custom property wrappers, you can significantly reduce boilerplate code and improve the modularity and reusability of your Swift projects

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

    References

  • From Zero to SOAP

    From Zero to SOAP

    SOAP (Simple Object Access Protocol) is often chosen over REST or GraphQL for scenarios requiring high security, reliability, and formal contracts, particularly in enterprise environments. Its built-in support for WS-Security, strong typing, and detailed error handling makes it ideal for industries like finance or healthcare that demand strict compliance and complex, stateful transactions. SOAP’s WSDL provides a clear, formal contract between client and server, ensuring interoperability across different platforms and legacy systems. However, REST and GraphQL are generally preferred for modern web applications due to their simplicity, flexibility, and lower overhead, making them more suitable for mobile or web-based services where performance and ease of use are prioritized. The choice ultimately depends on the specific requirements of the project, with SOAP excelling in structured, secure, and complex use cases.

    In previous post we have explores API REST, GraphQL and Websocket network aplicatuib interfaces, now is turn of SOAP. In this post we are going to develop a simple dockerized NodeJS SOAP server that offers an add callculation service.

    Addition SOAP Server

    First step is to build a SOAP server that will implement arithmethic add operation. The server technology used by its simpicity is a Dockerized NodeJS server. Let’s create a nodeJS server from scratch

    npm init -y

    Next install server dependencies for soap and express

    npm install soap express

    This is SOAP server itself:

    const express = require('express');
    const soap = require('soap');
    const http = require('http');
    
    const app = express();
    
    const service = {
      MyService: {
        MyPort: {
          AddNumbers: function (args) {
            const aValue = parseInt(args.a , 10);
            const bValue = parseInt(args.b , 10);
            console.log(' args.a + args.b',  aValue + bValue);
            return { result: aValue + bValue };
          },
        },
      },
    };
    
    const xml = `
    <definitions name="MyService"
      targetNamespace="http://example.com/soap"
      xmlns="http://schemas.xmlsoap.org/wsdl/"
      xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
      xmlns:tns="http://example.com/soap"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    
      <message name="AddNumbersRequest">
        <part name="a" type="xsd:int"/>
        <part name="b" type="xsd:int"/>
      </message>
      
      <message name="AddNumbersResponse">
        <part name="result" type="xsd:int"/>
      </message>
    
      <portType name="MyPort">
        <operation name="AddNumbers">
          <input message="tns:AddNumbersRequest"/>
          <output message="tns:AddNumbersResponse"/>
        </operation>
      </portType>
    
      <binding name="MyBinding" type="tns:MyPort">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="AddNumbers">
          <soap:operation soapAction="AddNumbers"/>
          <input>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </input>
          <output>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </output>
        </operation>
      </binding>
    
      <service name="MyService">
        <port name="MyPort" binding="tns:MyBinding">
          <soap:address location="http://localhost:8000/wsdl"/>
        </port>
      </service>
    </definitions>
    `;
    
    const server = http.createServer(app);
    server.listen(8000, () => {
      console.log('SOAP server running on http://localhost:8000/wsdl');
    });
    
    soap.listen(server, '/wsdl', service, xml);
    

    This Node.js script creates a SOAP web service using the express and soap libraries. The service, named MyService, exposes a single operation called AddNumbers, which takes two integer arguments (a and b), adds them together, and returns the result. The WSDL (Web Services Description Language) XML definition specifies how clients can interact with this service, including the request and response message structures, binding details, and the service endpoint (http://localhost:8000/wsdl). The server listens on port 8000, and when a SOAP request is received, it executes the AddNumbers operation and logs the sum to the console.

    The soap.listen() function attaches the SOAP service to the HTTP server, making it accessible to clients that send SOAP requests. The AddNumbers function extracts and parses the input arguments from the request, computes their sum, and returns the result in a SOAP response. This setup enables interoperability with SOAP-based clients that conform to the specified WSDL schema.

    Lets dockerize server:

    # Official image for Node.js
    FROM node:18
    
    # Fix working directory
    WORKDIR /app
    
    # Copy necessary files
    COPY package.json package-lock.json ./
    RUN npm install
    
    # Copy rest of files
    COPY . .
    
    # Expose server port
    EXPOSE 8000
    
    # Command for executing server
    CMD ["node", "server.js"]

    This Dockerfile sets up a containerized environment for running a Node.js application. It starts with the official Node.js 18 image as the base. The working directory inside the container is set to /app. It then copies the package.json and package-lock.json files into the container and runs npm install to install dependencies. After that, it copies the rest of the application files into the container. The file exposes port 8000, which is likely used by the Node.js server. Finally, it specifies the command to run the server using node server.js when the container starts.

    Build the image from command line:

    docker build -t soap-server .

    … and execute the image:

    docker run -p 8000:8000 soap-server

    Server ready, now is turn of Swift iOS App client.

    iOS SOAP Client

    iOS app client UI interface is just two textfield for fill in the input operator for the addition, a button for executing the operation and finally a text labed that presents the results.

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    When button is tapped then callSoaService  function is being called:

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    This Swift function callSoapService(a:b:completion:) sends a SOAP request to a web service at http://localhost:8000/wsdl, passing two integers (a and b) to the AddNumbers method. It constructs an XML SOAP message, sends it via an HTTP POST request, and processes the response asynchronously using URLSession. The response is parsed to extract the result from the <tns:result> tag using a regular expression in the extractResult(from:) function, returning it via a completion handler.

    If the web service is running correctly, it should return the sum of a and b. However, the function may fail if the server is unavailable, the response format changes, or if the SOAP service requires additional headers. Also, the regular expression parsing method may not be robust against namespace variations.

    Build and run iOS app:

     

    Conclusions

    SOAP does not provide the flexibility that provides a regular API REST or GraphQL, but for those scenarios wher do we need a very strict API contracts is quite suitable technology.

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

  • Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Implementing Sign in with Apple in a client-server setup is valuable because it addresses a real-world need that many developers face, especially as Apple requires apps offering third-party login to support it. While Apple’s documentation focuses mainly on the iOS side, there’s often a gap in clear explanations for securely validating Apple ID tokens on the backend — a critical step to prevent security vulnerabilities.

    Since Node.js is a widely used backend for mobile apps, providing a practical, end-to-end guide would help a large audience, fill a common knowledge gap, and position you as an expert who understands both mobile and server-side development, making the post highly useful, shareable, and relevant.

    Apple Sign in

    Apple Sign In is a secure authentication service introduced by Apple in 2019, allowing users to log in to apps and websites using their Apple ID. It emphasizes privacy by minimizing data sharing with third parties, offering features like hiding the user’s real email address through a unique, auto-generated proxy email. Available on iOS, macOS, and web platforms, it provides a fast and convenient alternative to traditional social logins like Google or Facebook.

    Advantages of Apple Sign In
    One of the biggest advantages is enhanced privacy—Apple does not track user activity across apps, and the «Hide My Email» feature protects users from spam and data leaks. It also simplifies the login process with Face ID, Touch ID, or device passcodes, reducing password fatigue. Additionally, Apple Sign In is mandatory for apps that offer third-party logins on iOS, ensuring wider adoption and consistent security standards.

    Inconveniences of Apple Sign In
    A major drawback is its limited availability, as it only works on Apple devices, excluding Android and Windows users. Some developers also criticize Apple for forcing its use on iOS apps while restricting competitor login options. Additionally, if a user loses access to their Apple ID, account recovery can be difficult, potentially locking them out of linked services. Despite these issues, Apple Sign In remains a strong choice for privacy-focused users.

    Dockerized Node.JS server side

    Start by setting up a blank Node.js server using Express.js to handle HTTP requests.

    npm init -y

    Server.js code is following:

    const express = require('express');
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware for parsing JSON
    app.use(express.json());
    
    // Client for look up public keys at Apple
    const client = jwksClient({
        jwksUri: 'https://appleid.apple.com/auth/keys'
    });
    
    // Function for getting public key
    function getAppleKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
            if (err) {
                callback(err);
            } else {
                const signingKey = key.getPublicKey();
                callback(null, signingKey);
            }
        });
    }
    
    // Route for authenticate
    app.post('/auth/apple', (req, res) => {
        const { identityToken } = req.body;
    
        if (!identityToken) {
            return res.status(400).json({ error: 'identityToken missing' });
        }
    
        jwt.verify(identityToken, getAppleKey, {
            algorithms: ['RS256']
        }, (err, decoded) => {
            if (err) {
                console.error('Error verifying token:', err);
                return res.status(401).json({ error: 'Invalid token' });
            }
    
            // decoded contains user data
            console.log('Token verified:', decoded);
    
            res.json({
                success: true,
                user: {
                    id: decoded.sub,
                    email: decoded.email,
                    email_verified: decoded.email_verified
                }
            });
        });
    });
    
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
    });
    

    server.js sets up an Express server that listens for authentication requests using Apple’s Sign-In service. It imports necessary modules like express for routing, jsonwebtoken for verifying JSON Web Tokens (JWTs), and jwks-rsa for retrieving Apple’s public keys used to validate tokens. The server is configured to parse incoming JSON payloads and uses environment variables (loaded via dotenv) to optionally define a custom port.

    The core logic resides in the /auth/apple POST route. When a client sends a request to this endpoint with an identityToken in the body (typically issued by Apple after a successful login), the server first checks if the token is present. It then verifies the token using jsonwebtoken.verify(), passing a custom key retrieval function (getAppleKey). This function uses the jwksClient to fetch the appropriate public key from Apple’s JWKS (JSON Web Key Set) endpoint based on the kid (Key ID) found in the token header.

    If the token is valid, the decoded payload—which includes user-specific data like sub (user ID), email, and email_verified—is extracted and returned in the response as JSON. If token verification fails, an error response with HTTP 401 status is sent. This setup allows backend applications to securely validate Apple identity tokens without hardcoding public keys, keeping the authentication mechanism both dynamic and secure.

    Server is dockerized:

    FROM node:20
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    This Dockerfile sets up a Node.js environment using the node:20 base image, creates a working directory at /usr/src/app, copies package.json and package-lock.json (if present) into it, installs dependencies with npm install, copies the rest of the application files, exposes port 3000 for the container, and finally runs the npm start command to launch the application.

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

    docker run -p 3000:3000 apple-signin-server

    Server ready for receiving requests…

    Client iOS Apple Sign in app

    After creating a simple iOS app project, go to the target settings and add the ‘Sign in with Apple’ capability. Then, start by creating a blank Node.js server.

    The next step is the client code itself:

    import SwiftUI
    import AuthenticationServices
    
    struct ContentView: View {
        @State private var userID: String?
        @State private var userEmail: String?
        @State private var userName: String?
        
        var body: some View {
            VStack(spacing: 20) {
                if let userID = userID {
                    Text("Welcome 🎉")
                        .font(.title)
                    Text("User ID: \(userID)")
                    if let name = userName {
                        Text("Name: \(name)")
                    }
                    if let email = userEmail {
                        Text("Email: \(email)")
                    }
                } else {
                    SignInWithAppleButton(
                        .signIn,
                        onRequest: { request in
                            request.requestedScopes = [.fullName, .email]
                        },
                        onCompletion: { result in
                            switch result {
                            case .success(let authorization):
                                handleAuthorization(authorization)
                            case .failure(let error):
                                print("Authentication error: \(error.localizedDescription)")
                            }
                        }
                    )
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 280, height: 50)
                    .cornerRadius(8)
                    .padding()
                }
            }
            .padding()
        }
        
        private func handleAuthorization(_ authorization: ASAuthorization) {
            if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                userID = appleIDCredential.user
                userEmail = appleIDCredential.email
                if let fullName = appleIDCredential.fullName {
                    userName = [fullName.givenName, fullName.familyName]
                        .compactMap { $0 }
                        .joined(separator: " ")
                }
                
                if let identityToken = appleIDCredential.identityToken,
                   let tokenString = String(data: identityToken, encoding: .utf8) {
                    authenticateWithServer(identityToken: tokenString)
                }
            }
        }
        
        private func authenticateWithServer(identityToken: String) {
            guard let url = URL(string: "http://localhost:3000/auth/apple") else { return }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = ["identityToken": identityToken]
            
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data,
                   let json = try? JSONSerialization.jsonObject(with: data) {
                    print("Server response:", json)
                } else {
                    print("Error communicating with server:", error?.localizedDescription ?? "Unknown error")
                }
            }.resume()
        }
    }
    
    
    #Preview {
        ContentView()
    }
    

    It defines a user interface for an iOS app that integrates Sign in with Apple. The core logic is built into the ContentView struct, which maintains state variables to store the signed-in user’s ID, name, and email. When the view is rendered, it checks whether the user is already signed in (i.e., if userID is not nil). If the user is authenticated, it displays a welcome message along with the retrieved user details. If not, it shows a «Sign in with Apple» button that initiates the authentication process when tapped.

    When the «Sign in with Apple» button is pressed, it triggers a request for the user’s full name and email. The result of this action is handled in the onCompletion closure. If the sign-in is successful, the handleAuthorization method is called. This function extracts the user’s credentials from the ASAuthorizationAppleIDCredential object, including their user ID, email, and full name (if provided). It also extracts the identity token (a JSON Web Token), which is used to authenticate the user on the app’s backend server.

    The authenticateWithServer function handles the server-side communication. It sends a POST request to http://localhost:3000/auth/apple, passing the identityToken in the JSON body. This token can be verified on the backend to ensure the identity is legitimate and secure. The response from the server (or any error encountered) is printed to the console. This architecture supports secure, privacy-preserving user login using Apple’s authentication services, commonly used in modern iOS apps.

    Apple Sign in integration

    Deploy iOS app with Apple Sign-In in a simulator (not on a real device).

    review

    Simply sign in using your personal iCloud credentials. Once Apple Sign-In is successful on the client side, it sends a request and provides the identityToken.

    Even if you uninstall the app from the device, the identityToken remains unchanged. Therefore, it can reliably be used as a user identifier.

    Conclusions

    From a programming perspective, implementing Apple Sign-In in your apps is straightforward and enhances privacy, as users can choose whether to share their email.

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

    References

  • Storing in the Sky: iCloud Integration for iOS

    Storing in the Sky: iCloud Integration for iOS

    This post.demystifies a powerful yet often underused feature in the Apple ecosystem. Many developers find iCloud integration—whether through CloudKit, key-value storage, or iCloud Drive—intimidating due to scattered documentation and complex setup. By offering a clear, beginner-friendly guide with a working example, you not only fill a common knowledge gap but also empower others to build more seamless, cloud-synced experiences across devices. It’s a great way to share practical knowledge, boost your credibility, and contribute to best practices in the iOS dev community.

    iCloud

    iCloud is Apple’s cloud-based storage and computing service that allows users to securely store data such as documents, photos, music, app data, and backups across all their Apple devices. It provides a seamless way to keep content in sync, making it accessible from iPhones, iPads, Macs, and even Windows PCs. With services like iCloud Drive, iCloud Photos, and iCloud Backup, users benefit from automatic data management and recovery options, which enhances their overall experience with Apple’s ecosystem.

    For app developers, integrating iCloud offers a range of benefits that can significantly improve user engagement and satisfaction. By using iCloud technologies such as CloudKit, developers can enable real-time data synchronization and seamless transitions between devices. For instance, a note taken on an iPhone can instantly appear on a Mac or iPad without requiring manual uploads or additional login steps. This functionality not only enhances user convenience but also opens doors for multi-device collaboration and continuity in usage.

    Moreover, iCloud integration can simplify backend infrastructure for developers. With CloudKit, developers don’t need to manage their own servers for syncing user data — Apple handles the storage, security, and data transfer. This reduces development time and operational overhead, while still providing users with fast, secure, and reliable cloud features. It also adds credibility to the app by aligning it with Apple’s high standards for privacy and performance, making iCloud integration a smart and strategic choice for apps within the Apple ecosystem.

    Setup iCloud on simulator

    For start working we need to fulfill 2 basic requirement: First one is having an iOS Development (or Enterprise) account for having access to iCloud console and second in iOS Simulator (or real device) be sure that you have sign in your Apple development account:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 10.30.52

    Last but not least, be sure that iCloud Drive, Sync this iPhone switch is on:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 10.31.29

    iOS Ranking app

    The app we are going to implement to demonstrate iCloud usage will allow users to enter their name and a point value. This app will be distributed across multiple user devices, enabling each user to submit their name and score. It will also display a global ranking based on the collected data.

    Once we have created our blank iOS project, on target signing & capabilities add iCloud:

    Add a new container:

    Type its container name, has to be prefixed by iCloud. :

    Ready:

    To update your app when changes occur in iCloud, you need to handle silent push notifications. By default, enabling the iCloud capability also includes Push Notifications. However, Background Modes are not enabled automatically—so be sure to add the Background Modes capability and check the «Remote notifications» option.

    For source code app we are going to focus only in CloudkitManager, view is very simple and doest apport too much. Nevertheless you will find code respository GitHub link at the end of the post:

    import CloudKit
    import Foundation
    
    
    class RankingViewModel: ObservableObject {
        @Published var scores: [PlayerScore] = []
        private var database = CKContainer(identifier: "iCloud.jca.iCloudRanking").publicCloudDatabase
        
        init() {
            fetchScores()
            setupSubscription()
    
            NotificationCenter.default.addObserver(
                forName: .cloudKitUpdate,
                object: nil,
                queue: .main
            ) { _ in
                self.fetchScores()
            }
        }
    
        func fetchScores() {
            let query = CKQuery(recordType: "Score", predicate: NSPredicate(value: true))
            let sort = NSSortDescriptor(key: "points", ascending: false)
            query.sortDescriptors = [sort]
    
            database.perform(query, inZoneWith: nil) { records, error in
                DispatchQueue.main.async {
                    if let records = records {
                        self.scores = records.map { PlayerScore(record: $0) }.sorted { $0.points > $1.points }
                        print("Fetching successfull")
                    } else if let error = error {
                        print("Error fetching scores: \(error.localizedDescription)")
                    }
                }
            }
        }
    
        func addScore(name: String, points: Int) {
            let record = CKRecord(recordType: "Score")
            record["name"] = name as CKRecordValue
            record["points"] = points as CKRecordValue
    
            database.save(record) { _, error in
                if let error = error {
                    print("Error saving score: \(error.localizedDescription)")
                } else {
                    print("Saving successfull")
                    DispatchQueue.main.async { [weak self] in
                        self?.localAddScore(record: record)
                    }
                }
            }
        }
        
        private func localAddScore(record: CKRecord) {
            
            scores.append(PlayerScore(record: record))
            scores = scores.sorted { $0.points > $1.points }
        }
        
        func setupSubscription() {
            let subscriptionID = "ranking-changes"
    
            let predicate = NSPredicate(value: true)
            let subscription = CKQuerySubscription(
                recordType: "Score",
                predicate: predicate,
                subscriptionID: subscriptionID,
                options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
            )
    
            let notificationInfo = CKSubscription.NotificationInfo()
            notificationInfo.shouldSendContentAvailable = true  // Silent
            subscription.notificationInfo = notificationInfo
    
            database.save(subscription) { returnedSub, error in
                if let error = error {
                    print("❌ Subscription error: \(error.localizedDescription)")
                } else {
                    print("✅ Subscription saved!")
                }
            }
        }
    }

    This Swift code defines a RankingViewModel class that interfaces with Apple’s CloudKit to manage a leaderboard-style ranking system. It fetches, updates, and stores player scores in an iCloud public database (iCloud.jca.iCloudRanking) using CloudKit. When the class is initialized, it automatically retrieves existing scores from CloudKit and sets up a subscription to receive real-time updates when scores are added, modified, or deleted. It also listens for a custom cloudKitUpdate notification and refetches scores when triggered. All fetched scores are stored in the @Published array scores, allowing SwiftUI views observing this view model to update dynamically.

    The fetchScores() function queries the CloudKit database for records of type «Score», sorting them by the number of points in descending order. These records are converted into PlayerScore instances (assumed to be a custom data model) and stored in the scores array. The addScore() function allows new scores to be submitted to the database. Once saved, the new score is locally appended and sorted in the scores array via localAddScore(). Additionally, the setupSubscription() method ensures the app receives silent push notifications when there are any changes to the «Score» records in CloudKit, keeping the leaderboard data synchronized across devices.

    When we deploy:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 11.50.47

    Issue, new ranking is not updated and we can read on Console log:

    For fixing that we have to make a few adjustments on iCloud Console.

    iCloud Console

    For having access to iCloud Console, just type ‘https://icloud.developer.apple.com/dashboard’ on your favourite browser and login with your Apple Developer (or Enterprise) account. Later on select the the iCloud containter that the app is being used:

    First step is creating a Record Type for taking a look at the recently uploaded user data:

    Next step is adding record fields (name and points):

    For being able to retrieve data from console we have to create a Querable Index on recordName field from Score Record Type:

    Now is time for checking previous stored data:

    For retrieve data from device, we have to create a Sortable Index for points filed in Score Record Type:

    When we deploy iOS app on a real device:

    screenshot

    Finally…

    For final validation of the iOS app concept, I deployed the app on two different physical devices. As demonstrated in the video, when a ranking is submitted on one device, the ranking list is updated almost instantly on the other device.

    Conclusions

    From a programming point of view, working with iCloud is relatively straightforward. What I’ve found a bit cumbersome, however, is setting up the iCloud Console. Overall, though, using iCloud is a good idea if you need to share data across all instances of your app.

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

    References

    • iCloud

      Apple Developer Documentation

  • Inside the iOS Sandbox: Managing Files and Folders

    Inside the iOS Sandbox: Managing Files and Folders

    Sandboxing in iOS is a foundational security mechanism that isolates each app in its own secure environment. This isolation prevents unauthorized access to system resources and user data, ensuring that apps cannot interfere with one another.

    For developers, understanding how to manage files and directories within this sandbox is crucial. It determines how and where persistent data, user-generated content, and temporary files are stored—directly affecting app functionality, user privacy, and compliance with App Store requirements.

    The goal of this post is to demystify these concepts. By doing so, it empowers developers to build secure, reliable, and user-friendly applications that align with iOS’s strict security model while effectively leveraging the available file system APIs.

    The Sandbox

    In iOS, a sandbox is a security mechanism that restricts apps to their own designated area, preventing them from accessing files, resources, or data belonging to other apps or the system without explicit permission. This isolation ensures stability, security, and privacy for users.

    Key Features of iOS Sandbox:

    1. App Isolation

      • Each app runs in its own sandboxed environment with its own directory for files.

      • Apps cannot directly read or modify files from other apps.

    2. Controlled Access to System Resources

      • Apps must request permissions (via entitlements or user consent) to access sensitive data like:

        • Contacts (Contacts.framework)

        • Photos (PHPhotoLibrary)

        • Location (CoreLocation)

        • Camera & Microphone (AVFoundation)

    3. File System Restrictions

      • Apps can only write files in their own sandbox directories, such as:

        • Documents/ (user-generated content, backed up by iTunes/iCloud)

        • Library/ (app support files, some backed up)

        • Caches/ (temporary files, can be purged by the system)

        • tmp/ (short-lived files, not backed up)

    4. No Direct Hardware or Kernel Access

      • Apps interact with hardware (e.g., GPU, sensors) only through Apple’s frameworks.

      • No root-level system modifications are allowed (unlike jailbroken devices).

    5. Inter-App Communication (Limited & Controlled)

      • Apps can share data only via:

        • URL Schemes (custom deep links like myapp://)

        • App Groups (shared containers for related apps)

        • UIActivityViewController (share sheets)

        • Universal Clipboard (limited-time data sharing)

    Why Does iOS Use a Sandbox?

    • Security: Prevents malware from spreading or tampering with other apps.

    • Privacy: Ensures apps access only permitted user data.

    • Stability: Crashes or bugs in one app don’t affect others.

    Example: Accessing the Sandbox in Code

    To get an app’s sandbox directory in Swift:

    struct PeopleView: View {
        @StateObject var viewModel = PeopleViewModel()
        
        var body: some View {
            NavigationView {
                ...
            }.onAppear {
                if let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
                    print("📂 Document Directory: \(documentsPath.path)")
                }
            }
        }
    }

    The snippet is used to retrieve and print the path to the app’s Documents directory on an iOS or macOS device. Its parent folder is the sandbox root folder for your current app.

    Exceptions to the Sandbox:

    • System apps (e.g., Messages, Mail) have broader privileges.

    • Jailbroken devices bypass sandbox restrictions (but violate Apple’s policies).

    The sandbox is a core reason iOS is considered more secure than open platforms. Developers must work within its constraints while using Apple’s APIs for permitted interactions.

    Our privacy is compromised the moment a malicious app can access another app’s sandbox. Theoretically, this kind of sandbox breach hasn’t been documented on iOS—at least not to my knowledge. However, the video «Broken Isolation – Draining Your Credentials from Popular macOS Password Managers« by Wojciech Reguła (NSSpain 2024) demonstrates how, on macOS, a malicious app can gain access to the sandboxes of other apps that store user passwords—such as NordPass, KeePass, Proton Pass, and even 1Password.

    Sandbox data container folders

    Each iOS app has its own container directory with several subdirectories. Here’s a breakdown of the key folders and their purposes:

    1. Documents

    • Path: .../Documents/

    • Purpose: Stores user-generated content or data that should persist and be backed up to iCloud.

    • Example: Saved notes, PDFs, exported data.

    • Backup: ✅ Included in iCloud/iTunes backups.

    • Access: Read/Write.

    2. Library

    • Path: .../Library/

    • Purpose: Stores app-specific files and configuration data.

      It has two main subfolders:

      • Preferences

        • .../Library/Preferences/

        • Stores user settings (e.g., using UserDefaults).

        • Managed automatically by the system.

      • Caches

        • .../Library/Caches/

        • Used for data that can be regenerated (e.g., image cache).

        • Not backed up, and iOS may delete files here when space is low.

        • ⚠️ Don’t store critical data here.

    4. tmp

    • Path: .../tmp/

    • Purpose: Temporary files your app doesn’t need to persist between launches.

    • Backup: ❌ Not backed up.

    • Auto-clean: iOS may clean this directory at any time.

    • Access: Read/Write.

    Summary Table

    FolderPurposePersistentBacked UpiOS May Delete
    App BundleApp code and resources
    DocumentsUser data/files
    Library/PreferencesApp settings (UserDefaults)
    Library/CachesCached data (non-critical)
    tmpTemporary files

     

     

    Files operations

    For this section, we have developed a sample iOS application that performs storage operations using files. The app displays an empty list with an «Add» button in the navigation bar. Each time the button is pressed, a new person is added to the list. The list of people serves as the model and is persisted as a .json file.

    When we deploy on simulator (or real device):

    The component that handles files operations:

    class FileManagerHelper {
        static let shared = FileManagerHelper()
        
        private let fileName = "people.json"
        
        private var fileURL: URL {
            let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            return documents.appendingPathComponent(fileName)
        }
    
        func save(_ people: [Person]) {
            do {
                let data = try JSONEncoder().encode(people)
                try data.write(to: fileURL)
            } catch {
                print("Error saving file: \(error)")
            }
        }
        
        func load() -> [Person] {
            do {
                let data = try Data(contentsOf: fileURL)
                let people = try JSONDecoder().decode([Person].self, from: data)
                return people
            } catch {
                print("Error reading file: \(error)")
                return []
            }
        }
        
        func deleteFile() {
            do {
                try FileManager.default.removeItem(at: fileURL)
            } catch {
                print("Error deleting file: \(error)")
            }
        }
    }

    FileManagerHelper is a singleton utility that manages saving, loading, and deleting a JSON file named people.json in the app’s documents directory. It provides methods to encode an array of Person objects into JSON and save it to disk (save), decode and return the array from the saved file (load), and remove the file entirely (deleteFile). It handles errors gracefully by catching exceptions and printing error messages without crashing the app.

    Conclusions

    With that post, I just intended to give you an overview and demonstrate how easy it is to deal with file persistence as well.

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

    References

  • QR Code Scanning in iOS

    QR Code Scanning in iOS

    Scanning a URL encoded within a QR code and fetching its contents is particularly interesting because it addresses a common real-world scenario faced by developers. QR codes are widely used for sharing links, event details, and more, making it essential for iOS apps to handle them efficiently.

    This post will not only guide developers through implementing QR code scanning using native frameworks like AVFoundation but also demonstrate how to seamlessly fetch and process the retrieved URL content using URLSession.

    We will implement a client-server application where the server will provide a QR image and implement REST API services encoded within the QR code. The client will be an iOS app that scans the QR code and fetches the REST API service.

    QR-Server

    For the server, we will implement a Dockerized Node.js server to create a blank project.

    npm init -y

    Later on, we will need to integrate ‘express’ and ‘qrcode’ into the project.

    npm install express qrcode

    Server code is following:

    const express = require('express');
    const QRCode = require('qrcode');
    const os = require('os');
    
    const app = express();
    const port = 3000;
    const hostIP = process.env.HOST_IP || 'Desconocida';
    
    app.get('/', (req, res) => {
        const url = `http://${hostIP}:${port}/data`;
        QRCode.toDataURL(url, (err, qrCode) => {
            if (err) res.send('Error generating QR code');
            res.send(`<!DOCTYPE html><html><body>
            <h2>Scan the QR code:</h2>
            <img src="${qrCode}" />
            </body></html>`);
        });
    });
    
    app.get('/data', (req, res) => {
        res.json({ message: 'Hello from QR-API-Server!' });
    });
    
    app.listen(port, () => {
        console.log(`Server running on http://${hostIP}:${port}`);
    });

    This Node.js script sets up an Express.js web server that generates a dynamic QR code. When the root URL («/») is accessed, the server creates a QR code containing a URL pointing to the /data endpoint. The QR code is displayed on an HTML page with the message «Scan the QR code.»

    Accessing the /data endpoint returns a JSON object with the message: «Hello from QR-API-Server!». The server listens on port 3000, and the host IP address is either obtained from an environment variable or defaults to 'Desconocida' (Unknown) if not specified.

    Next step: set up the Dockerfile.

    # Base image for Node.js
    FROM node:14
    
    # Create and fix working dirctory
    WORKDIR /usr/src/app
    
    # Copy application files
    COPY . .
    
    # Install dependencies
    RUN npm install
    
    # Expose appliction port
    EXPOSE 3000
    
    # Command for starting server application
    CMD ["node", "server.js"]

    The Dockerfile sets up a Docker image for a Node.js application. It starts by using the official Node.js 14 base image. It then creates and sets the working directory to /usr/src/app. The application files from the local directory are copied into this working directory. Next, it installs the necessary dependencies using npm install. The image exposes port 3000, indicating that the application will listen on that port. Finally, the Docker container will run the Node.js server application by executing node server.js when started.

    Get back to command line and create docker image:

    docker build -t qr-server .

    And finally run the image:

    docker run -d -p 3000:3000 \ 
    -e HOST_IP=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -n 1) qr-server

    You need to provide the host Docker machine’s container runner IP to allow an iOS app running on a real device (due to camera scanning) to access the server.

    QR Scaner iOS App

    Client is an iOS sample Scan app designed to scan QR codes and call the service endpoint encoded within the QR code. To perform scanning, the app will require access to the camera.

    Open target build settings and fill in ‘Privacy – Camera Usage Description’. View code is following:

    struct ContentView: View {
        @State private var scannedURL: String? = nil
        @State private var dataFromAPI: String? = nil
        @State private var showAlert = false
        @State private var alertMessage = ""
    
        var body: some View {
            VStack {
                if let scannedURL = scannedURL {
                    Text("URL scanned: \(scannedURL)")
                        
                        .padding()
    
                    Button("Do API Call") {
                        Task {
                            await fetchAPIData(from: scannedURL)
                        }
                    }
                    .padding()
    
                    if let dataFromAPI = dataFromAPI {
                        Text("Data from API: \(dataFromAPI)")
                            .padding()
                    }
                } else {
                    ZStack {
                        
                        QRCodeScannerView {
                            self.scannedURL = $0
                        }
                        .edgesIgnoringSafeArea(.all)
                        Text("Scan QR code:")
                    }
    
                }
            }
            .font(.title)
            .alert(isPresented: $showAlert) {
                Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
            }
        }
    
        func fetchAPIData(from url: String) async {
            guard let url = URL(string: url) else { return }
    
            do {
                let (data, response) = try await URLSession.shared.data(from: url)
                if let result = String(data: data, encoding: .utf8) {
                    dataFromAPI = result
                }
            } catch {
                alertMessage = "Error: \(error.localizedDescription)"
                showAlert = true
            }
        }
    }

    SwiftUI code creates a ContentView that first scans a QR code to extract a URL, displays the scanned URL, and provides a button to fetch data from that URL via an API call, showing the result or an error alert if the request fails. The interface initially shows a QR scanner overlay with the prompt «Scan QR code,» and upon successful scanning, it displays the URL and a button to trigger the API call, which asynchronously retrieves and displays the data or shows an error message in an alert if something goes wrong. The layout uses a vertical stack (VStack) to organize the UI elements and adjusts fonts and padding for better readability.

    QRCodeScannerView has to be implemented by using UIKit-UIViewControllerRepresentable bridge compontent:

    import SwiftUI
    import AVFoundation
    
    struct QRCodeScannerView: UIViewControllerRepresentable {
        class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
            var parent: QRCodeScannerView
    
            init(parent: QRCodeScannerView) {
                self.parent = parent
            }
    
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    parent.didFindCode(stringValue)
                }
            }
        }
    
        var didFindCode: (String) -> Void
    
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                return viewController
            }
    
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            Task {
                captureSession.startRunning()
            }
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    }

    The code creates a QR code scanner view that uses AVFoundation to capture and process QR codes, where it sets up a camera preview layer, configures a capture session to detect QR codes, and triggers a vibration and callback function (didFindCode) when a QR code is successfully scanned, passing the decoded string value to the parent view. The UIViewControllerRepresentable protocol bridges UIKit’s AVCaptureMetadataOutput functionality into SwiftUI, with a Coordinator class handling the metadata output delegation to process detected codes.

    Important point for scanning you need to deploy on a real iOS device:

    As soon as the QR code URL is read, it is presented to the user. When the user presses ‘Do API Call’, the iOS app performs a request to the service.

    Conclusions

    In this post I have presented how to consume API services where endpoints are could be dynamically provided by server via QR codes. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

  • Decluter Your Codebase: Periphery for Dead Code Detection

    Decluter Your Codebase: Periphery for Dead Code Detection

    Periphery addresses a common challenge faced by developers: maintaining clean, efficient codebases. Periphery, an open-source tool, offers a powerful solution for identifying and eliminating unused code in Swift projects, which can significantly improve app performance, reduce compile times, and enhance overall code quality.

    In this post we will explain how to setup Periphery on your iOS project for later on force any code situations to trgger expected warnings.

    Setup Periphery

    First step is install periphery by using homebrew installation method:

    brew install periphery

    Next step is creating juts a regular iOS Sample app with XCode: 

    brew install periphery

    Move to the folder where the project was created and type:

    And setup periphery for current iOS project:

    periphery scan --setup

    This will generate a hidden configuration file.

    Get back to XCode Project:

    And select the target, Build Phases, add (+) and NewScriptPhase. Do not forget unchecking ‘Based on dependency analysis’ option.

    Script basically consists in calling periphery for performing and scan. Press CMD+B for build and check if setup was ok:

    For fixing this issue just set User Script Sandboxing to No on Build Settings:

    Turn to build again for checking that now thi time all works fine:

    Detecting dead code

    Periphery aims to detect and report unused declarations in your code. These declarations include classes, structs, protocols, functions, properties, constructors, enums, typealiases, and associated types. As expected, Periphery can identify straightforward cases, such as a class that is no longer referenced anywhere in your codebase. For the purpose of this post we have added some dead code for checking that warnings are really trigger every time we build de code (CMD+B).

    Now appears new warnings that aims to dead code, but appart from thead code this utility also allows to detectct unused parameters:

    Conclusions

    Periphery is one of those tools that its setup effort is very low and will help you on removing dead code. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

    References

  • Beyond JSON Codables

    Beyond JSON Codables

    Explore decoding not just JSON but also CSV, XML, Plist, and YAML using Codable is interesting because developers often work with diverse data formats beyond JSON. While Codable is primarily designed for JSON, showcasing how to extend its functionality to handle other formats efficiently can help developers streamline their data parsing workflow.

    In this post, we will build an iOS sample app that will present how to parse codable data text format in JSON, CSV, XML, Plist and Yaml.

    JSON

    JSON (JavaScript Object Notation) is a lightweight data format used for storing and exchanging data in a human-readable and structured way. It is widely used because of its simplicity, readability, and compatibility with various programming languages. JSON represents data as key-value pairs, similar to a JavaScript object, making it easy to parse and generate. It is commonly used in web APIs, configurations, and data storage due to its efficiency, flexibility, and seamless integration with modern web technologies.

    Parsing code is implemented along with with its presentation:

    struct JSONView: View {
        @State private var people: [Person] = []
    
           func loadPeople() {
               let json = """
               [
                   {"name": "Juan", "age": 30},
                   {"name": "Ana", "age": 25},
                   {"name": "Carlos", "age": 35}
               ]
               """
               
               let data = Data(json.utf8)
               
               do {
                   let decodedPeople = try JSONDecoder().decode([Person].self, from: data)
                   self.people = decodedPeople
               } catch {
                   print("\(error)")
               }
    
           }
    
           var body: some View {
               NavigationView {
                   PeopleListView(people: people)
                   .navigationTitle("Persons List (JSON)")
               }
               .task {
                   loadPeople()
               }
           }
    }

    This SwiftUI code defines a JSONView struct that loads and displays a list of people from a hardcoded JSON string. The loadPeople() function decodes the JSON into an array of Person objects and assigns it to the @State variable people. The body property presents a NavigationView containing a PeopleListView, passing the people array to it. The .task modifier ensures loadPeople() runs asynchronously when the view appears, populating the list.

    XML

    XML (Extensible Markup Language) is a structured text format used to store and transport data in a human-readable and machine-readable way. It organizes data using custom tags that define elements hierarchically, making it widely used for data exchange between systems. XML is closely related to SOAP (Simple Object Access Protocol), as SOAP messages are formatted using XML. SOAP is a protocol for exchanging structured information in web services, relying on XML to define message structure, including headers and body content. This enables platform-independent communication between applications over protocols like HTTP and SMTP.

    XML is not supperted natively, so we have to import an SPM package such as SWXMLHash:

    Code for parsing and presenting:

    struct XMLView: View {
        @State private var people: [Person] = []
    
        func loadPeople() {
            let xmlString = """
                    <Persons>
                        <Person>
                            <Name>Teresa</Name>
                            <Age>35</Age>
                        </Person>
                        <Person>
                            <Name>Ana</Name>
                            <Age>45</Age>
                        </Person>
                        <Person>
                            <Name>Carlos</Name>
                            <Age>35</Age>
                        </Person>
                    </Persons>
                    """
    
            let xml = XMLHash.config { _ in }.parse(xmlString)
    
            do {
                let fetchedPeople: [Person] = try xml["Persons"].children.map { element in
                    let name: String = try element["Name"].value() ?? ""
                    let age: Int = try element["Age"].value() ?? -1
                    return Person(name: name, age: age)
                }
                people = fetchedPeople
            } catch {
                print("Error decoding XML: \(error)")
            }
        }
    
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                    .navigationTitle("Persons List (XML)")
            }
                .task {
                loadPeople()
            }
        }
    }

    The XMLView SwiftUI struct parses an XML string containing a list of people and displays them in a PeopleListView. It defines a @State variable people to store the parsed data and a loadPeople() function that uses the XMLHash library to extract names and ages from the XML. The parsed data is then stored in people, which updates the UI. The body consists of a NavigationView that displays PeopleListView, and loadPeople() is called asynchronously using .task {} when the view appears. This setup ensures that the list is populated dynamically from the XML data.

    CSV

    CSV (Comma-Separated Values) is a widely used data text file format because it is simple, lightweight, and universally compatible across different software and programming languages. It stores tabular data in plain text, making it easy to read, edit, and process without requiring specialized software. CSV files are also highly efficient for data exchange between applications, databases, and spreadsheets since they maintain a structured yet human-readable format. Additionally, their lack of complex metadata or formatting ensures broad support and ease of integration in data processing workflows.

    Parse in case of this file is are very simple string processing operations:

    struct CSVView: View {
        @State private var people: [Person] = []
        
        func loadPeople() {
            let csvString = """
                    name,age
                    Ricardo,40
                    Priscila,25
                    Carlos,35
                    """
    
            let lines = csvString.components(separatedBy: "\n")
                var persons: [Person] = []
    
                for line in lines.dropFirst() { // Remove header
                    let values = line.components(separatedBy: ",")
                    if values.count == 2, let age = Int(values[1]) {
                        persons.append(Person(name: values[0], age: age))
                    }
                }
            people = persons
        }
        
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                .navigationTitle("Persons List (CSV)")
            }
            .task {
                loadPeople()
            }
        }
    }

    The CSVView struct in SwiftUI loads a hardcoded CSV string containing names and ages, parses it into an array of Person objects, and displays them using PeopleListView. It first defines a @State variable people to store the parsed data. The loadPeople() function splits the CSV string into lines, ignores the header, extracts name and age values, converts them into Person objects, and updates people. The body contains a NavigationView that presents PeopleListView, and the .task modifier ensures loadPeople() runs when the view appears, allowing dynamic data population.

    Yaml

    YAML is a common data text file format because it is human-readable, easy to write, and supports complex data structures like lists and key-value mappings in a simple, indentation-based syntax. It is widely used for configuration files, data serialization, and automation scripts in DevOps, Kubernetes, and CI/CD pipelines due to its readability compared to JSON and XML. Additionally, YAML supports comments, anchors, and references, making it more flexible for structured data representation while remaining easy to integrate with various programming languages.

    Yaml, as well as XML, is also not supperted natively, so we have to import an SPM package such as Yams:

    Code for parsing and presenting:

    struct YamlView: View {
        @State private var people: [Person] = []
    
           func loadPeople() {
               let json = """
               - name: Sebastián
                 age: 32
               - name: Ana
                 age: 26
               - name: Pedro
                 age: 35
               """
               
               let data = Data(json.utf8)
               
               do {
                   let decodedPeople = try YAMLDecoder().decode([Person].self, from: data)
                   self.people = decodedPeople
               } catch {
                   print("\(error)")
               }
    
           }
    
           var body: some View {
               NavigationView {
                   PeopleListView(people: people)
                   .navigationTitle("Persons List (Yaml)")
               }
               .task {
                   loadPeople()
               }
           }
    }

    This SwiftUI view, YamlView, loads a list of people from a YAML-formatted string and displays them in a PeopleListView. It uses @State to store an array of Person objects and defines a loadPeople() function that converts a hardcoded YAML string into a Data object, decodes it into an array of Person structs using YAMLDecoder(), and updates the state. In the body, it presents a NavigationView with PeopleListView, setting «Persons List (Yaml)» as the navigation title. The .task modifier ensures loadPeople() runs asynchronously when the view appears.

    PList

    Last but not least, and old fatigue companiong that get along with us during many years in XCode projects.PLIST (Property List) is a common data text file format, especially in Apple’s ecosystem, because it is human-readable, structured, and easily parsed by both machines and developers. It supports hierarchical data storage, making it ideal for configuration files, preferences, and serialization in macOS and iOS applications. PLIST files can be formatted in XML or binary, allowing flexibility in readability and performance. Their native support in Apple frameworks, such as Core Foundation and Swift, makes them a default choice for storing structured data in a standardized way.

    A god point it that is supported natively:

    struct Plist: View {
        @State private var people: [Person] = []
    
        func loadPeople() {
            let plist = """
               <?xml version="1.0" encoding="UTF-8"?>
               <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
               <plist version="1.0">
                   <array>
                       <dict>
                           <key>name</key>
                           <string>Juan Pérez</string>
                           <key>age</key>
                           <integer>30</integer>
                       </dict>
                       <dict>
                           <key>name</key>
                           <string>Ana Gómez</string>
                           <key>age</key>
                           <integer>25</integer>
                       </dict>
                       <dict>
                           <key>name</key>
                           <string>Sílvia</string>
                           <key>age</key>
                           <integer>55</integer>
                       </dict>
                   </array>
               </plist>
               """
    
            let data = Data(plist.utf8)
    
            do {
                let decodedPeople = try PropertyListDecoder().decode([Person].self, from: data)
                self.people = decodedPeople
            } catch {
                print("\(error)")
            }
    
        }
    
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                    .navigationTitle("Persons List (Plist)")
            }
                .task {
                loadPeople()
            }
        }
    }

    This SwiftUI View named Plist loads a hardcoded Property List (Plist) containing an array of people, decodes it into an array of Person objects using PropertyListDecoder(), and displays the list using a PeopleListView. The loadPeople() function parses the embedded XML-based Plist data, converting it into Data, decodes it into an array of Person structs, and assigns it to the @State variable people. The body of the view contains a NavigationView that initializes PeopleListView with the decoded list and sets the navigation title. The .task modifier ensures that loadPeople() runs when the view appears, populating the list dynamically. If decoding fails, an error message is printed.

    Conclusions

    In this project, we have seen the most common data text formats and how to parse them. You can find source code used for writing this post in following repository

  • 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