Autor: admin

  • Switching App Language on the Fly in Swift

    Switching App Language on the Fly in Swift

    Dynamically switching languages in views and using snapshot testing for multilingual validation it addresses a common challenge in global app development: ensuring seamless localization. Developers often struggle with updating UI text instantly when users change languages, and validating UI consistency across multiple locales can be tedious.

    We will cover dynamic language switching for streamlining the user experience, on the other side snapshot testing ensures visual accuracy without manual verification.

    Multilanguage iOS app

    After creating an iOS sample we are going to include the language catalog:

    Next step is adding a new language:

    For this example we have chosen Spanish:

    Next step we start to fill in string catalog for all languages defined:

    Language controller

    For implementing languange controller we will make use of Observer pattern, that means that this class state changes then all views subscribed to it will get notified and will be refreshed automatically:

    class LocalizationManager: ObservableObject {
        @Published var locale: Locale = .current {
            didSet {
                UserDefaults.standard.set([locale.identifier], forKey: "AppleLanguages")
                UserDefaults.standard.synchronize()
            }
        }
        
        init() {
            if let preferredLanguage = UserDefaults.standard.array(forKey: "AppleLanguages")?.first as? String {
                locale = Locale(identifier: preferredLanguage)
            }
        }
        
        func toggleLanguage() {
            if locale.identifier == "en" {
                locale = Locale(identifier: "es")
            } else {
                locale = Locale(identifier: "en")
            }
        }
    }

    This Swift class, LocalizationManager, is an ObservableObject that manages the app’s language settings. It stores the current locale in UserDefaults under the key "AppleLanguages", ensuring that the language preference persists across app restarts. The locale property is @Published, so any UI elements observing it will update when the language changes. The initializer retrieves the stored language preference from UserDefaults or defaults to the system’s current locale. The toggleLanguage() method switches between English ("en") and Spanish ("es") by updating locale, which in turn updates UserDefaults. However, changing this value does not dynamically update the app’s language in real-time without restarting.

    Finally build and run application:

    Multilanguage view

    View implementation is following:

    struct ContentView: View {
        @StateObject private var localizationManager = LocalizationManager()
        
        var body: some View {
            VStack {
                Text("welcome_message".localized(with: localizationManager.locale))
                    .padding()
                
                Button(action: {
                    localizationManager.toggleLanguage()
                }) {
                    Text("change_language".localized(with: localizationManager.locale))
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            .environmentObject(localizationManager)
        }
    }

    This SwiftUI ContentView uses a @StateObject called LocalizationManager to manage language localization. It displays a localized welcome message and a button that allows users to toggle the app’s language. The Text elements use a .localized(with:) function to fetch translated strings based on the current locale from LocalizationManager. When the button is tapped, it calls toggleLanguage(), which presumably switches between different languages. The localizationManager is also injected into the SwiftUI environment using .environmentObject(), making it accessible throughout the app.

    Finally, cleaner code on view, and it is a practice that I have seen in many projects is to create an extension for finally  retrieve string translated, but this time with the add on that is parametrized with location language code:

    extension String {
        func localized(with locale: Locale) -> String {
            let language = locale.identifier
            guard let path = Bundle.main.path(forResource: language, ofType: "lproj"),
                  let bundle = Bundle(path: path) else {
                return NSLocalizedString(self, comment: "")
            }
            return bundle.localizedString(forKey: self, value: nil, table: nil)
        }
    }

    The extension adds a localized(with:) function to the String type, allowing it to return a localized version of the string based on the specified Locale. It first retrieves the locale’s language identifier and attempts to find the corresponding .lproj resource bundle in the app’s main bundle. If the bundle exists, it fetches the localized string from it; otherwise, it falls back to NSLocalizedString, which returns the string from the app’s default localization. This enables dynamic localization based on different languages.

    Snapshot testing

    Last but not least, apart from implementing a user interface that allows user to switch language when something is not propery understood on app regular usage. It opens up a new tesging scenario, we are possible to validate the same view presented in different languages, this will you make easier to detect texts that were not presented properly. 

    In Visual Regression Testing: Implementing Snapshots test on iOS post I present how to setup and implement snapshoptesting, is very easy setup. So basically we will bypass snapshot setup library and we will focus just only on sntapshots tests:

    @Suite("Snapshot tests")
    struct DynamicLanguageTests {
    
        let record = true
    
        @Test func testContentViewInEnglish() {
                let localizationManager = LocalizationManager()
                localizationManager.locale = Locale(identifier: "en")
                
                let contentView = ContentView()
                    .environmentObject(localizationManager)
                    .frame(width: 300, height: 200)
                
                let viewController = UIHostingController(rootView: contentView)
                
                assertSnapshot(
                    of: viewController,
                    as: .image(on: .iPhoneSe),
                    named: "ContentView-English",
                    record: record
                )
            }
    
        @Test  func testContentViewInSpanish() {
                let localizationManager = LocalizationManager()
                localizationManager.locale = Locale(identifier: "es")
                
                let contentView = ContentView()
                    .environmentObject(localizationManager)
                    .frame(width: 300, height: 200)
                
                let viewController = UIHostingController(rootView: contentView)
                
                assertSnapshot(
                    of: viewController,
                    as: .image(on: .iPhoneSe),
                    named: "ContentView-Spanish",
                    record: record
                )
            }
    }

    The DynamicLanguageTests struct, annotated with @Suite("Snapshot tests"), contains two test functions: testContentViewInEnglish() and testContentViewInSpanish(). Each function sets up a LocalizationManager with a specific locale (en for English, es for Spanish), creates a ContentView instance, embeds it in a UIHostingController, and captures a snapshot of the rendered UI on an iPhone SE device. The snapshots are named accordingly («ContentView-English» and «ContentView-Spanish») and compared against previously recorded images to detect unintended visual changes.

    Conclusions

    Dynamic Language Switch it could be an interesting implementation for allowing user swap view language for beter understaning., but also is more useful when we have to validate that a view is properly presente in all app languages, you can find the source code used for this post in the following repository

  • Bitrise Magic: One-Click iOS Builds for Your QA Team

    Bitrise Magic: One-Click iOS Builds for Your QA Team

    Automating iOS app distribution for QA and reviewers using Bitrise addresses a common pain point—manually sharing builds—by streamlining the process through CI/CD. Bitrise simplifies generating signed IPAs (for ad-hoc testing), uploading to TestFlight, and deploying to third-party services like Firebase App Distribution, all while handling code signing, versioning, and notifications automatically.

    This post is a step-by-step guide to help developers set up Bitrise to generate an iOS build artifact for distribution to QA or any reviewer.

    Getting in context

    To develop this post, we need the app for internal distribution, an iOS Developer Program license from Apple, and a free Bitrise account.

    The app we will distribute internally is an iOS project called SMOC. SMOC is a head-up display speedometer and dash camera. The app builds without issues, and all unit tests pass. While this may seem obvious, it’s important to ensure that build crashes do not occur during workflow executions.

    Being a member of the Apple Developer Program is mandatory because, at some point during the Bitrise setup, you will need to connect to the Apple Developer portal with your credentials. Additionally, you will need to provide an API key (.p8 file) to Bitrise, generated in the Apple Developer portal and also the Development Certificate (.p12 file).

    Bitrise offers a 30-day free trial account that allows you to experiment with and initiate iOS app internal distribution.

    Generating Apple Developer API Keys

    Login in into your Apple developer account:

    Select User and Access. On the incoming screen, choose Integrations > App Store Connect API from the left-side menu. Under Equipment Keys, click the Add (+) button to create a new key.

    Important point: Access must be set to ‘Administrative’; I was not successful with the ‘Developer’ role. Download the API key (.p8 file) to a secure location, as we will need it to configure Bitrise. The ID Key and Issuer ID have been masked for security reasons, but make sure to take note of them because you will need them later.

    In addition to the API key (.p8 file), we also need the Apple Development Certificate (.p12 file). To obtain it, open Keychain and export the certificate. Remember the password you set during the export process, as it will be required when importing the certificate into Bitrise.

    If the certificate is not in your keychain, you can create it via Xcode. Go to Settings > Accounts > Manage Certificates, and select Apple Development. The certificate will automatically be added to your local machine’s keychain and uploaded to the Apple Developer Portal.

    General iOS Setup

    Once logged into your Bitrise account, the first step—before configuring any project—is to set up some global Bitrise configurations that will be needed later. Since our codebase is hosted on GitHub, we will connect our GitHub account.

    Now it’s time to add your API key. Go back to the previous screen, select ‘App Store Connect,’ and press the ‘Add API Key’ button.

    The name is an identifier of your choice, but the Issuer ID and Key ID are obtained when you create an API key on the Apple Developer portal. Finally:

    Following the ‘least privilege principle’, I initially created an API key with a Developer role. However, the workflow execution failed due to insufficient access rights, so I had to generate a new API key, this time with an Administrator role.

    Lastly, we need to connect the Bitrise account with our Apple Developer account, just as we did with GitHub. However, this time, we need to go to the top-right user icon menu.

    And connect with your Apple Developer (or Enterprise) account:

    iOS Project setup

    Now is the time to create a new project. Select the dashboard and click the ‘(+) New Project’ button.

    Select ‘Add your App’:

    Select ‘New project’:

    And press Next.

    Fill in the app title, mobile platform, and app bundle ID. Then, select ‘Integrations’ from the left menu.

    And just indicate which API key you are going to use.

    Go to the Dashboard and select ‘Configure’ in the project you’re working on.

    Set up the project and access. In our case, we aim to provide CI builds to team members who either do not have access to Bitrise or are not permitted to use it.

    In our case, the code repository is GitHub, so we set the URL to point to our repository.

    Bitrise authorization is not strictly necessary because the repository is public. However, if it were private, we would need to authorize access. For the «Choose Branch» step, we select «No, skip auto-detection and configure settings manually» because, although the default branch is develop, developers often distribute builds to their QA colleagues based on their working branches rather than the default branch.

    In the ‘Default configuration’, we set the project type, project (or workspace) filename, scheme, and distribution method. In our case, we are interested in distributing builds based on development to our QA team.

    Next, we select the XCode version to be used as the toolchain and build machine. Due to our account subscription, we are limited to working with only one type of machine.

    Set the app icon to add an avatar to the project, then select ‘Register a Webhook’ for me!

    Configuration finished just press ‘View CI configuration’.

    Configuration is almost complete, but not quite. Next, we need to upload the Development Certificate (.p12 file). To do this, select ‘Code Signing’ and then click ‘Add .p12 File.

    Once added:

    Even though we set the API key previously, I faced some compilation crashes that failed due to the API key, so I had to specify again which API key I was going to use.

    Project configuration is ready. Now, select workflows to create our workflow, which is also known as pipelines in other contexts.

    Workflow creation

    On workflows select ‘(+) Create Workflow’:

    Fill in the name of the workflow, as well as the workflow on which it is based. In our case, it is ‘archive_and_export_app’:

    This would be ok in case our project used Cocoapods as a dependency manager, but is our case is not.

    So we have to remove Cocoapods step:

    Save workflow:

    Workflow execution

    Setup work finished, now is time to run the workflow, press ‘Start build’ button:

    Press ‘Start build’:

    After a few minutes….

    If everything is set up correctly, the ‘Success’ message should appear. Now, press the ‘Right arrow’ icon to continue with the build distribution.

    Install and run

    We need to provide the app download URL, either by distributing a QR code or sharing the direct link. Our responsibilities as developers are complete.

    As a QA reviewer, once we scan the QR code or enter the link in the device browser, we will see the following. The setup process will only occur the first time the app is installed. Subsequent re-installations (or removal and reinstallation) will not trigger the setup process again.

    Select ‘Check compatibility’ and following alert will appear:

    Select Allow for installing configutation profile. And Open Device Settings, General:

    Select Allow for installing configutation profile. And Open Device Settings, General, VPN & Device Management:

    And select ‘Install’. Remember this setup has to be done once per device and app distributed.

    And yes, finally press install to begin installing the app on your device. The following animation will show you how the app is installed and execute:

    Conclusions

    Although the configuration process can be tedious, once it’s set up, the developer only needs to focus on executing the workflow on the desired working branch and providing the workflow or artifact distribution link to QA or any other interested reviewer. On the QA side, they simply need to click the link to install the app and start testing.

    References

    • Bitrise

      The CI/CD Platform built for Mobile DevOps

    • SMOC

      Portfolio – On board car clip camera iOS App

    • SMOC

      Apple Store

  • 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

  • Visual Regresion Testing: Implementing Snapshot test on iOS

    Visual Regresion Testing: Implementing Snapshot test on iOS

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

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

    Setup iOS Project

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

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

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

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

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

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

    Snapshop testing

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

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

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

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

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

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

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

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

    Run the test and now all must be green:

    Do test fail!

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

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

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

    With your favourite folder content comparator compare both folders:

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

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

    Conclusions

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

    References

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

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

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

    In this post we will revieew 5 principles by examples.

    S.O.L.I.D. principles

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

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

    SRP-Single responsability principle

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

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

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

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

    OCP-Open/Close principle

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

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

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

    LSP-Liksov substitution principle

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

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

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

    ISP-Interface segregation principle

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

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

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

    DIP-Dependency injection principle

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

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

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

    Conclusions

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

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

    Copy-on-Write in Swift: Managing Value Types

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

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

    CoW – Copy-on-Write

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

    How Copy on Write Works

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

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

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

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

    Benefits

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

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

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

    Implementation

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

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

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

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

    Example code this time is provided via Playground:

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

    Key Components:

    1. Wrapper Class:

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

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

    2. CoWExample Struct:

      • Contains a private property storage of type Wrapper.

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

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

    3. modifyData() Method:

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

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

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

      • Appends the value 4 to the data array.

    4. Memory Address Tracking:

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

    Run playground

    Lets run the playground and analyze logs:

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

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

    Conclusions

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

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

    References

  • Enhancing iOS Apps with Redis

    Enhancing iOS Apps with Redis

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

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

    Redis server

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

    npm init -y

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

    npm install dotenv express ioredis

    Node.Js server is following:

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

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

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

    Sensitive intormateion is stored  in .env file:

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

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

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

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

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

    node_modules
    npm-debug.log
    .env
    .git
    

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

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

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

    Build image and deploy containers:

    docker-compose up --build -d

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

    Backend side is ready!

    iOS Sample App

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

    Core app functionallity is following:

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

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

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

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

    Conclusions

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

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

    References

  • Opaque types in Swift

    Opaque types in Swift

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

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

    Opaque types

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

    Lets start by a generic sample:

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

    In this example:

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

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

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

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

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

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

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

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

    any keyword

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

    We will continue with previous generic example:

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

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

    On extending previous SwiftUI example with any:

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

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

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

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

    Conclusions

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

    References