Autor: admin

  • Integrating Vapor into Native macOS Apps

    Integrating Vapor into Native macOS Apps

    What if your next backend server didn’t live in the cloud, but right inside your user’s Applications folder? By embedding a Vapor REST API inside a native macOS app, you can build powerful, cloud-free local tools using 100% Swift.

    This architecture showcases the ultimate power of a unified Swift ecosystem, allowing you to develop the frontend UI, backend APIs, and shared data models within a single language and toolchain. It unlocks highly demanded, «local-first» use cases—such as building local smart home hubs, custom developer tooling, or Wi-Fi companion apps for iOS—all without the cost, complexity, or latency of cloud infrastructure.

    In this post, we will walk through the entire process from scratch: creating a native macOS app, configuring the project’s Vapor dependencies, and writing the core concurrency code to get your sample desktop REST server up and running.

    Creating macOS app from scratch

    To create your macOS app, simply open Xcode and select the native macOS App template project to get started:

    Screenshot

    Next, build and run the project to check the application’s initial appearance.

    Screenshot

    Setting Up the Vapor Server Component

    Vapor is an open-source, asynchronous web framework written in Swift that allows developers to build backend services—such as REST APIs, web apps, and WebSocket servers—using the same modern, type-safe language they use for iOS and macOS development. Instead of relying on a separate tech stack like Node.js, Python, or Go, Vapor enables you to write your entire application end-to-end in Swift.

    To integrate the framework into our macOS application, we will import the Vapor library via Swift Package Manager (SPM). To do this, simply navigate to your project’s Package Dependencies tab within Xcode’s project settings.

    Screenshot

    Enter https://github.com/vapor/vapor in the search field to locate the Vapor package.

    Screenshot

    Make sure to add the Vapor package to your MacVaporApp target before completing the setup.

    Screenshot

    Enabling these network checkboxes is to configure the macOS App Sandbox security system, granting your application the essential network permissions to function as a web server. By default, macOS isolates applications and blocks network access for security reasons. Checking «Incoming Connections (Server)» allows your embedded Vapor backend to bind to a port (like 8085) and listen for incoming HTTP requests, preventing the server from crashing due to permission errors upon startup. Meanwhile, enabling «Outgoing Connections (Client)» permits your application to send the HTTP responses back to the requesting clients and allows your app to communicate with external APIs or remote databases, effectively transforming your secure desktop app into a fully operational and communicative web server.

    Screenshot

    Hands on code

    The first compnente that we are to develop is ServerManager class:

    import Vapor
    import Foundation
    import Combine
    
    class ServerManager: ObservableObject {
        @Published var isRunning = false
        private var vaporApp: Vapor.Application?
        
        func startServer() {
            guard !isRunning else { return }
            
            Task.detached(priority: .background) {
                do {
                    var env = Environment.development
                    env.arguments = ["vapor"]
                    
                    let app = Vapor.Application(env)
                    
                    app.http.server.configuration.port = 8085
                    
                    app.get("hello", ":name") { req -> String in
                        guard let name = req.parameters.get("name") else {
                            throw Abort(.badRequest)
                        }
                        return "hello \(name)"
                    }
                    
                    await MainActor.run {
                        self.vaporApp = app
                        self.isRunning = true
                    }
                    
                    try await app.execute()
                    
                } catch {
                    print("Error en el servidor: \(error)")
                    await MainActor.run { self.isRunning = false }
                }
            }
        }
        
        func stopServer() {
            guard isRunning else { return }
            vaporApp?.shutdown()
            self.isRunning = false
            self.vaporApp = nil
        }
    }

    This class controls the lifecycle of an embedded Vapor web server alongside an app’s UI by safely managing background threads. When startServer() is called, it spins up a new instance of a Vapor web application on a detached background thread to prevent blocking the main user interface, configures it to listen on port 8085, and registers a dynamic REST endpoint (GET /hello/:name) that greets the user by name. The class uses MainActor.run to safely update its @Published state variable (isRunning) on the main thread so that user interface views can reactively update their status, and it provides a stopServer() method to gracefully shut down the server and clean up its memory when no longer needed.

    import SwiftUI
    
    struct ContentView: View {
        @ObservedObject var manager: ServerManager
        
        var body: some View {
            VStack(spacing: 25) {
                HStack {
                    Circle()
                        .fill(manager.isRunning ? Color.green : Color.red)
                        .frame(width: 14, height: 14)
                    
                    Text(manager.isRunning ? "REST Server Active" : "REST Server Offline")
                        .font(.title3)
                        .fontWeight(.medium)
                }
                
                if manager.isRunning {
                    Text("Listening on: http://localhost:8085/hello/{name}")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                
                HStack(spacing: 20) {
                    Button(action: { manager.startServer() }) {
                        Text("Start Server")
                            .padding(.horizontal, 10)
                    }
                    .disabled(manager.isRunning)
                    .keyboardShortcut("s", modifiers: .command)
                    
                    Button(action: { manager.stopServer() }) {
                        Text("Stop Server")
                            .padding(.horizontal, 10)
                    }
                    .disabled(!manager.isRunning)
                    .keyboardShortcut("d", modifiers: .command)
                }
            }
            .frame(width: 450, height: 220)
            .padding()
        }
    }

    This code defines a SwiftUI ContentView that serves as the visual control panel for your macOS desktop application, reactively updating its interface based on the state of the embedded ServerManager. It renders a vertically stacked layout featuring a dynamic status indicator (a green circle for active or a red circle for offline) alongside descriptive text, and conditionally displays the active local endpoint URL only when the server is running. Finally, it provides two horizontally arranged buttons to start and stop the server, which automatically toggle their enabled states to prevent redundant clicks and support native macOS keyboard shortcuts (Cmd + S to start, Cmd + D to stop) for a seamless user experience.

    curl http://localhost:8085/hello/Alice
    Screenshot

    Conclusions

    In this post, we successfully embedded a Vapor REST server inside a native macOS desktop application with ease. You can find the complete source code used for this project in the repository repository

    References

  • Dockerizing a C Component with Vapor

    Dockerizing a C Component with Vapor

    In our previous posts, we explained how to run an ANSI C component on iOS and Android. This time, we are moving to a backend platform using Vapor.

    This post covers the necessary add-ons for the C code and details how to configure a Vapor project to implement a service that consumes the exported component functionality. This setup will allow you to develop and debug the backend locally. Finally, I will explain how to deploy the backend using Docker.

    The Core ANSI C Functionality

    Our goal is to cross-compile and export this ANSI C functionality beyond iOS and Android to include a Vapor-based backend server.

    #include "core_component.h"
    #include <stdio.h>
    
    /**
     * Gets the version in X.Y.Z format.
     * * @param out_version Pointer to the buffer where the version string will be stored.
     * @param max_len     Maximum size of the buffer.
     * @return            0 if successful, or -1 if the buffer is too small.
     */
    int getVersion(char *out_version, size_t max_len) {
        /* Simulated version numbers */
        int major = 1;
        int minor = 4;
        int patch = 12;
        
        /* Safely format the string and prevent buffer overflows */
        int written = snprintf(out_version, max_len, "%d.%d.%d", major, minor, patch);
    
        /* Check if the string was truncated or if an encoding error occurred */
        if (written < 0 || (size_t)written >= max_len) {
            return VERSION_ERR_INSUFFICIENT_BUF; 
        }
    
        return VERSION_SUCCESS; 
    }

    Create a blank Vapor Backend project

    To create a blank Vapor sample project, follow these steps:

    Screenshot

    For our purposes, we do not need an ORM, database support, or Leaf HTML rendering.

    Setting Up the ANSI C Component for Vapor

    The philosophy behind this series of posts is to share the same functionality across multiple platforms without ever sharing the underlying source code. The following script provides the ANSI C binaries for the Vapor backend server:

    #!/bin/bash
    
    # Stop the script if any error occurs
    set -e
    
    echo "🚀 Starting compilation of core_component for Vapor..."
    
    # 1. Define relative paths
    SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
    OUTPUT_DIR="$SCRIPT_DIR/build_vapor"
    BACKEND_DIR="$SCRIPT_DIR/../SwiftBackend"
    
    # NEW: Target paths following the native SPM standard
    BACKEND_LIBS_DIR="$BACKEND_DIR/Libs"
    BACKEND_INCLUDE_DIR="$BACKEND_DIR/Sources/SwiftBackend/include"
    
    # 2. Create output folders if they do not exist
    mkdir -p "$OUTPUT_DIR"
    mkdir -p "$BACKEND_LIBS_DIR"
    mkdir -p "$BACKEND_INCLUDE_DIR" # 👈 NEW: Creates the 'include' folder inside Vapor's source code
    
    # 3. Compile the C code into an object file (.o)
    echo "📦 Compiling core_component.c..."
    gcc -c "$SCRIPT_DIR/core_component.c" -o "$OUTPUT_DIR/core_component.o" -fPIC
    
    # 4. Package the object file into a static library (.a)
    echo "📚 Creating static library libcore_component.a..."
    ar rcs "$OUTPUT_DIR/libcore_component.a" "$OUTPUT_DIR/core_component.o"
    
    # 5. Copy the required files to the Vapor project
    echo "🚚 Copying binaries to the libraries directory..."
    cp "$OUTPUT_DIR/libcore_component.a" "$BACKEND_LIBS_DIR/"
    
    echo "📂 Copying header (.h) to the native Target structure (Sources/.../include)..."
    cp "$SCRIPT_DIR/core_component.h" "$BACKEND_INCLUDE_DIR/"
    
    echo "✅ Process completed successfully!"
    echo "Binary copied to: $BACKEND_LIBS_DIR"
    echo "Header copied to: $BACKEND_INCLUDE_DIR"

    Run the script form coreC folder and check that all is working fine:

    Screenshot

    Consuming the ANSI C Component in Vapor

    The first file we need to update in the Vapor project is Package.swift:

    // swift-tools-version:6.0
    import PackageDescription
    import Foundation
    
    let package = Package(
        name: "SwiftBackend",
        platforms: [
           .macOS(.v13)
        ],
        dependencies: [
            // 💧 A server-side Swift web framework.
            .package(url: "https://github.com/vapor/vapor.git", from: "4.115.0"),
            // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
            .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
        ],
        targets: [
            .executableTarget(
                name: "SwiftBackend",
                dependencies: [
                    .product(name: "Vapor", package: "vapor"),
                    .product(name: "NIOCore", package: "swift-nio"),
                    .product(name: "NIOPosix", package: "swift-nio"),
                ],
                swiftSettings: swiftSettings,
                linkerSettings: [
                    .unsafeFlags([
                        "-L", "\(URL(fileURLWithPath: #filePath).deletingLastPathComponent().path)/Libs"
                    ]),
                    .linkedLibrary("core_component")
                ]
            ),
            .testTarget(
                name: "SwiftBackendTests",
                dependencies: [
                    .target(name: "SwiftBackend"),
                    .product(name: "VaporTesting", package: "vapor"),
                ],
                swiftSettings: swiftSettings
            )
        ]
    )
    
    var swiftSettings: [SwiftSetting] { [
        .enableUpcomingFeature("ExistentialAny"),
    ] }

    It defines a Swift Package Manager (Package.swift) configuration for a server-side backend application named SwiftBackend, setting up a macOS executable that depends on the Vapor web framework and SwiftNIO networking libraries. Specifically, the linkerSettings block instructs the compiler where to find and how to integrate external binaries not written in Swift; the .unsafeFlags line dynamically determines the absolute path to a local directory named Libs relative to this configuration file and passes it via the -L search flag so the linker knows where to look, while .linkedLibrary("core_component") explicitly tells the linker to bind the precompiled static or dynamic library file (such as libcore_component.a) located inside that folder into the final executable.

    Next is bridging header file:

    Screenshot

    The SwiftBackend.h file is necessary because it serves as the umbrella header required by Swift Package Manager (SPM) to bridge C and Swift. SPM strictly mandates that a mixed-language target contains a header file matching the exact name of the target (SwiftBackend) inside its include directory to define the module’s public C interface. By writing #include "mi_componente.h" inside it, this file acts as the official gateway that exposes your underlying C functions and the compiled static library (libcore_component.a) to your Swift code, allowing them to interface seamlessly without compilation errors.

    Finally, implement the service that provides access to the ANSI C component:

        app.get("version") { req -> String in
            // Allocate an output buffer array safe for an X.Y.Z string
            let bufferSize = 32
            var outputBuffer = [Int8](repeating: 0, count: bufferSize)
            
            // Execute the mapped global C function
            let statusCode = c_getVersion(&outputBuffer, bufferSize)
            
            // Process the return code matching your C macro definitions
            switch statusCode {
            case 0: // VERSION_SUCCESS
                return String(cString: outputBuffer)
            case -1: // VERSION_ERR_INSUFFICIENT_BUF
                throw Abort(.internalServerError, reason: "Error from C: Insufficient buffer size")
            default:
                throw Abort(.internalServerError, reason: "Unknown native version fetching error")
            }
        }

    This code defines a Vapor HTTP GET route handler at the /version endpoint that safely exposes an underlying C function to the web. It allocates a fixed-size, 32-byte integer array (outputBuffer) to safely receive a string from the C function, then executes c_getVersion by passing a reference to this buffer and its maximum size. Finally, it evaluates the returned status code using a switch statement: if successful (case 0), it converts the null-terminated C string buffer into a native Swift String and returns it as the HTTP response, whereas if an error occurs (such as an insufficient buffer size or an unknown failure), it throws a structured HTTP 500 Internal Server Error using Vapor’s Abort mechanism.

    Finally check in any browser the service response:

    Screenshot

    This is a debugable Vapor project, but not deployable yet.

    Dockerizing the Vapor Server

    Once the Vapor backend server is ready for release, let’s build the Docker image:

    # ================================
    # Build image
    # ================================
    FROM swift:6.1-noble AS build
    
    # Install OS updates and build tools
    RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
        && apt-get -q update \
        && apt-get -q dist-upgrade -y \
        && apt-get install -y libjemalloc-dev build-essential
    
    # Set base root folder
    WORKDIR /build
    
    # 1. Copy manifest respecting the project structure
    COPY ./SwiftBackend/Package.* ./SwiftBackend/
    
    # We are going the fix dependencies for doing that we move to project folder
    WORKDIR /build/SwiftBackend
    RUN swift package resolve \
            $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)
    
    # 2. Get back to the compilation folder for pouring real code
    WORKDIR /build
    COPY ./coreC ./coreC
    COPY ./SwiftBackend ./SwiftBackend
    
    # 3. Provide execution permission and run the script
    RUN chmod +x ./coreC/build_linux.sh && ./coreC/build_linux.sh
    
    # 4. Get back to SwiftBackend for executiong oficial vapor compilation
    WORKDIR /build/SwiftBackend
    
    RUN mkdir /staging
    
    # Build the application, with optimizations, with static linking, and using jemalloc
    RUN --mount=type=cache,target=/build/SwiftBackend/.build \
        swift build -c release \
            --product SwiftBackend \
            --static-swift-stdlib \
            -Xlinker -ljemalloc && \
        # Copy main executable to staging area
        cp "$(swift build -c release --show-bin-path)/SwiftBackend" /staging && \
        # Copy resources bundled by SPM to staging area
        find -L "$(swift build -c release --show-bin-path)" -regex '.*\.resources$' -exec cp -Ra {} /staging \;
    
    
    # Switch to the staging area
    WORKDIR /staging
    
    # Copy static swift backtracer binary to staging area
    RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
    
    # Adjust verification resource routes because we are into/build/SwiftBackend
    RUN [ -d /build/SwiftBackend/Public ] && { mv /build/SwiftBackend/Public ./Public && chmod -R a-w ./Public; } || true
    RUN [ -d /build/SwiftBackend/Resources ] && { mv /build/SwiftBackend/Resources ./Resources && chmod -R a-w ./Resources; } || true
    
    # ================================
    # Run image
    # ================================
    FROM ubuntu:noble
    
    RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
        && apt-get -q update \
        && apt-get -q dist-upgrade -y \
        && apt-get -q install -y \
          libjemalloc2 \
          ca-certificates \
          tzdata \
        && rm -r /var/lib/apt/lists/*
    
    RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
    
    WORKDIR /app
    
    COPY --from=build --chown=vapor:vapor /staging /app
    
    ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
    
    USER vapor:vapor
    
    EXPOSE 8080
    
    ENTRYPOINT ["./SwiftBackend"]
    CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
    

    This Dockerfile implements a multi-stage build tailored to securely and efficiently compile and run your Vapor backend while incorporating your external C component. The first stage (build) utilizes a full Swift 6.1 environment on Ubuntu Noble to install required compilation tools (like build-essential and libjemalloc-dev), resolve Swift package dependencies, and fetch cache layers. It intentionally copies your proprietary source code (./coreC and ./SwiftBackend), grants execution permissions to run your standalone compilation script (build_linux.sh) to build the C binaries safely in isolation, and compiles the final Vapor executable optimized for production (-c release) with memory-efficient static linking (--static-swift-stdlib, -ljemalloc). All necessary runtime assets, public resources, and the static backtracer are then consolidated into a temporary /staging directory.

    The second stage (run) creates a stripped-down, secure production image starting from a clean Ubuntu Noble base, completely throwing away the compiler tools and your original .c and .swift source code to minimize the final container size and protect your intellectual property. It installs only minimal required runtime libraries (libjemalloc2, certificates, and timezone data) and sets up an unprivileged, isolated system user named vapor. The final steps pull exclusively the compiled binaries and public assets from the first stage’s /staging area into the /app folder under strict vapor user ownership, configures production environment variables for safety and crash-tracking, exposes port 8080, and commands the container to run the production Vapor server on 0.0.0.0:8080.

    Let’s build the Docker image:

    docker build -t swift-backend-app -f SwiftBackend/Dockerfile .
    Screenshot

    Deploy a docker container with the image generated:

    docker run -d -p 8080:8080 --name mi-servidor-vapor swift-backend-app
    Screenshot

    This time, instead of using a web browser, we will test the service using the curl command:

    curl -i "http://localhost:8080/version"
    Screenshot
    docker run -d -p 8080:8080 --name mi-servidor-vapor swift-backend-app

    Conclusions

    n this post, we successfully migrated our ANSI C functionality to a backend platform. Throughout this three-part series, we have demonstrated how to reuse a single binary component across iOS, Android, and a server-side framework like Vapor.

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

  • Multiplatform ANSI C on Android

    Multiplatform ANSI C on Android

    In our previous post, we explained how to run an ANSI C component on iOS. In this second part, we will walk through the exact same process for an Android app.

    Building on that foundation, this post covers the necessary add-ons for the ANSI C code, how to configure Android Studio, and how to implement an Android app that seamlessly utilizes the ANSI C component.

    The Core ANSI C Functionality

    Here is the function we exported to iOS:

    #include "core_component.h"
    #include <stdio.h>
    
    /**
     * Gets the version in X.Y.Z format.
     * * @param out_version Pointer to the buffer where the version string will be stored.
     * @param max_len     Maximum size of the buffer.
     * @return            0 if successful, or -1 if the buffer is too small.
     */
    int getVersion(char *out_version, size_t max_len) {
        /* Simulated version numbers */
        int major = 1;
        int minor = 4;
        int patch = 12;
        
        /* Safely format the string and prevent buffer overflows */
        int written = snprintf(out_version, max_len, "%d.%d.%d", major, minor, patch);
    
        /* Check if the string was truncated or if an encoding error occurred */
        if (written < 0 || (size_t)written >= max_len) {
            return VERSION_ERR_INSUFFICIENT_BUF; 
        }
    
        return VERSION_SUCCESS; 
    }

    Configuring Android Studio for a New App

    As an iOS developer, this is my first post exploring Android technology. We will be using Android Studio as our IDE. Once you download it, a bit of extra configuration is required: specifically, you will need to install the NDK and CMake SDK tools within Android Studio.

    Screenshot

    These tools are not utilized directly by Android Studio, but are required by the build script to compile the ANSI C code for the Android application.

    Next, create a new project and choose the Empty Activity template:

    Screenshot

    Next are the project settings:

    Screenshot

    Make sure to pay close attention to the Package name—our upcoming code and scripts are going to depend on it!

    Because the Android ecosystem is so huge, you won’t see any devices listed the first time you try to run your app. To get a device set up, just head over to Tools -> Device Manager:

    Screenshot

    For this case I have selected ‘Pixel 8’:

    Screenshot

    Now, you should have no problem running the app on the emulator.

    Setting Up the ANSI C Component for Android

    First, we will implement the bridge file between the two technologies. This file is called core_component_jni.c and should be placed alongside the rest of your .c files:

    #include <jni.h>
    #include <stdlib.h>
    #include "core_component.h"
    
    // Replace "com_example_myapp" with your Android app's actual package (using underscores)
    JNIEXPORT jstring JNICALL
    Java_com_example_myapplication_NativeCore_getNativeVersion(JNIEnv *env, jobject thiz) {
    
        char buffer[32]; // Buffer to store "X.Y.Z"
        
        // We call your original ANSI C function
        int result = getVersion(buffer, sizeof(buffer));
        
        if (result == VERSION_SUCCESS) {
            // We convert the C char* to a Java/Kotlin jstring
            return (*env)->NewStringUTF(env, buffer);
        } else {
            return (*env)->NewStringUTF(env, "Error: Insufficient buffer");
        }
    }

    This C code acts as a JNI (Java Native Interface) bridge that allows an Android application written in Kotlin or Java to safely communicate with your underlying ANSI C library. Specifically, it defines a native function named getNativeVersion mapped to the NativeCore object inside the com.example.myapplication package. When invoked from the Android side, the function allocates a local 32-byte character buffer and executes your core C function, getVersion(), to write the software’s version numbers into it. Finally, it evaluates the operation’s success: if successful, it safely converts the standard C string (char*) into a Java-compatible string object (jstring) using the JNI environment pointer (*env), and if it fails, it gracefully returns an error message string back to Kotlin.

    Next, we have the build_android.sh script, which compiles and packages the ANSI C component so it can be consumed by Android:

    #!/bin/bash
    set -e
    
    # 1. Define the base path of the Android SDK on your Mac
    SDK_PATH="$HOME/Library/Android/sdk"
    
    # 2. Automatically detect the highest installed NDK version
    if [ -d "$SDK_PATH/ndk" ]; then
        NDK_VERSION=$(ls -1 "$SDK_PATH/ndk" | sort -V | tail -n 1)
        NDK_PATH="$SDK_PATH/ndk/$NDK_VERSION"
    else
        echo "❌ Error: 'ndk' folder not found at $SDK_PATH/ndk"
        exit 1
    fi
    
    TOOLCHAIN="$NDK_PATH/build/cmake/android.toolchain.cmake"
    
    echo "🤖 NDK Automatically detected at: $NDK_PATH"
    echo "📄 Toolchain: $TOOLCHAIN"
    
    # 3. Path configuration for the Multiplatform Project
    # Since the script runs from 'common/', we go up one level and enter AndroidApp
    ANDROID_APP_JNI_DIR="../AndroidApp/app/src/main/jniLibs"
    
    ARCHS=("armeabi-v7a" "arm64-v8a" "x86" "x86_64")
    ANDROID_API=21
    
    echo "🧹 Cleaning up previous local builds and target directories in AndroidApp..."
    rm -rf build_android jniLibs
    rm -rf "$ANDROID_APP_JNI_DIR"
    
    # Ensure target folders exist
    mkdir -p jniLibs
    mkdir -p "$ANDROID_APP_JNI_DIR"
    
    for ARCH in "${ARCHS[@]}"
    do
        echo "------------------------------------------------"
        echo "🤖 Generating environment for: $ARCH..."
        mkdir -p "build_android/$ARCH"
        
        cmake -B "build_android/$ARCH" \
              -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \
              -DANDROID_ABI="$ARCH" \
              -DANDROID_PLATFORM=android-$ANDROID_API \
              -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
    
        echo "🛠️ Compiling private binary ($ARCH)..."
        cmake --build "build_android/$ARCH" --target core_component_c_shared --config Release > /dev/null 2>&1
        
        # Locate the newly created .so file
        SO_FILE=$(find "build_android/$ARCH" -name "libcore_component_c_shared.so" | head -n 1)
        
        if [ -z "$SO_FILE" ]; then
            echo "❌ Error: The .so file was not generated for $ARCH"
            exit 1
        fi
        
        # Copy 1: Local backup in the 'coreC/jniLibs' folder
        mkdir -p "jniLibs/$ARCH"
        cp "$SO_FILE" "jniLibs/$ARCH/"
        
        # Copy 2: Direct deployment into the AndroidApp directory structure
        mkdir -p "$ANDROID_APP_JNI_DIR/$ARCH"
        cp "$SO_FILE" "$ANDROID_APP_JNI_DIR/$ARCH/"
        
        echo "✅ Binary for $ARCH successfully copied to AndroidApp."
    done
    
    echo "------------------------------------------------"
    echo "🎉 ABSOLUTE VICTORY! Process completed."
    echo "🚀 The private binaries are ready at: $ANDROID_APP_JNI_DIR"

    This Bash script automates the multi-architecture compilation and deployment of your native C library so it can be consumed by an Android application. It begins by locating your Mac’s Android SDK path, dynamically detecting the highest installed Android NDK version to extract the required CMake toolchain file, and cleaning up any legacy build directories. Then, it loops through a predefined array of target CPU architectures (armeabi-v7a, arm64-v8a, x86, and x86_64), invoking CMake for each one to configure and compile the Release binary (libcore_component_c_shared.so) specifically tailored to Android API level 21. Finally, it locates each freshly generated .so file and copies it to two destinations: a local backup directory (jniLibs) and directly into the Android project’s source tree (app/src/main/jniLibs), ensuring that the Android app has all the native binaries it needs for cross-platform hardware compatibility.

    Run the script to verify that everything is working correctly:

    Screenshot

    Consuming the ANSI C Component in Android

    Now, let’s make the final adjustments to run the component inside our Android app. To ensure that Android Studio correctly packages the .so files your script copied into jniLibs, open the app-level build.gradle.kts file and verify that the source path is mapped inside the android block:

    android {
        ...
    
        sourceSets {
            getByName("main") {
                // Le dice a Gradle que busque los .so en la carpeta que llenó tu script
                jniLibs.srcDirs("src/main/jniLibs")
            }
        }
    }

    Just as we did in the iOS tutorial (Part 1), we will create a Kotlin wrapper for the ANSI C functionality. Create a file named NativeCore.kt under the com.example.myapp package:

    package com.example.myapplication
    
    object NativeCore {
        init {
            // Loads the .so library. Note that the "lib" prefix and the ".so" extension are omitted
            System.loadLibrary("core_component_c_shared")
        }
    
        /**
         * Declares the native method. The 'external' keyword indicates
         * to Kotlin that the implementation is in native code (C/C++).
         */
        external fun getNativeVersion(): String
    }

    The final step is to update the MainActivity to call the exported function and display the version:

    package com.example.myapplication
    
    import android.app.Activity
    import android.os.Bundle
    import android.widget.TextView
    
    class MainActivity : Activity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val versionDesdeC = NativeCore.getNativeVersion()
    
            findViewById<TextView>(R.id.myTextView).text = "Version CoreC: $versionDesdeC"
        }
    }

    Deploy app on simulator:

    Screenshot

    Conclusions

    Now, our cross-platform architecture is truly taking form! We officially have a shared ANSI C core powering both our iOS and Android apps. Frameworks like Flutter, React Native, or Kotlin Multiplatform are fantastic for building user interfaces and high-level business logic, but ANSI C is still King when it comes to raw performance, heavy lifting, and total portability.

    Implementing a common ANSI C component is incredibly valuable for scenarios like:

    • High-Performance Computation & Math

    • Protecting Proprietary Algorithms

    • Low-Level Audio & Video Processing

    • Reusing Massive, Pre-Existing Open Source Libraries

    • Establishing a Single Source of Truth

    In our next and final post of this series, we’re going to take this a step further. We will move this exact same ANSI C component to the backend and deploy it inside a dockerized Vapor application!

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

  • Multiplatform ANSI C on iOS

    Multiplatform ANSI C on iOS

    SynapseC is the heart of this post. The main idea here is to connect different frontend and backend worlds using a central «nervous system» written in ANSI C. We are going to build a simple native C component that acts as the shared core logic for iOS, Android, and a Dockerized Vapor server. One of the best parts about this approach? We never expose the raw ANSI C code to other platforms. The end consumer will only ever deal with a clean binary, never the source code.

    In this first part of our three-part series, we’ll kick things off by building that common ANSI C core and plugging it into an iOS app as an SPM package.

    Core ANSI C component

    The IDE of choice for this ANSI C project is Visual Studio Code. To build and run the code seamlessly, these are the essential extensions I used:

    As a starting point, we will create a simple function that returns the component’s version. This will be highly useful for future troubleshooting and tracking as the project grows.

    #include "core_component.h"
    #include <stdio.h>
    
    /**
     * Gets the version in X.Y.Z format.
     * * @param out_version Pointer to the buffer where the version string will be stored.
     * @param max_len     Maximum size of the buffer.
     * @return            0 if successful, or -1 if the buffer is too small.
     */
    int getVersion(char *out_version, size_t max_len) {
        /* Simulated version numbers */
        int major = 1;
        int minor = 4;
        int patch = 12;
        
        /* Safely format the string and prevent buffer overflows */
        int written = snprintf(out_version, max_len, "%d.%d.%d", major, minor, patch);
    
        /* Check if the string was truncated or if an encoding error occurred */
        if (written < 0 || (size_t)written >= max_len) {
            return VERSION_ERR_INSUFFICIENT_BUF; 
        }
    
        return VERSION_SUCCESS; 
    }

    Our main.c entry point will handle running some unit tests. Granted, this isn’t the ideal architecture for a growing project—as it would quickly become unbearable to maintain—but for this post, it gets the job done perfectly.

    Screenshot

    Now, let’s run the code to ensure the syntax is correct and our unit tests pass successfully.

    Generate SPM component

    Once we have assured that component code sanity. Next step is generate the SPM, for make it simplier we have created following script:

    #!/bin/bash
    set -e
    
    # 0. Assure script is run from its own directory (robustly)
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    cd "$SCRIPT_DIR"
    
    # 1. Clean up previous artifacts
    echo "🧹 Cleaning up previous builds..."
    rm -rf "$SCRIPT_DIR/build_ios" \
           "$SCRIPT_DIR/build_sim_arm" \
           "$SCRIPT_DIR/build_sim_x86" \
           "$SCRIPT_DIR/build_xcf" \
           "$SCRIPT_DIR/CoreC.xcframework" \
           "$SCRIPT_DIR/Package.swift"
    
    # 2. Compile for Physical Device (iOS arm64) using native Makefiles
    echo "📱 Compiling for iOS device (arm64)..."
    cmake -B "$SCRIPT_DIR/build_ios" \
      -G "Unix Makefiles" \
      -DCMAKE_SYSTEM_NAME=iOS \
      -DCMAKE_OSX_ARCHITECTURES=arm64 \
      -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
    
    cmake --build "$SCRIPT_DIR/build_ios" --target core_c_lib --config Release
    echo "✅ iOS device built successfully!"
    
    # 3. Compile for Simulator (arm64)
    echo "💻 Compiling for iOS Simulator (arm64)..."
    cmake -B "$SCRIPT_DIR/build_sim_arm" \
      -G "Unix Makefiles" \
      -DCMAKE_SYSTEM_NAME=iOS \
      -DCMAKE_OSX_SYSROOT=iphonesimulator \
      -DCMAKE_OSX_ARCHITECTURES=arm64 \
      -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
    
    cmake --build "$SCRIPT_DIR/build_sim_arm" --target core_c_lib --config Release > /dev/null 2>&1
    
    # 4. Compile for Simulator (x86_64)
    echo "💻 Compiling for iOS Simulator (x86_64)..."
    cmake -B "$SCRIPT_DIR/build_sim_x86" \
      -G "Unix Makefiles" \
      -DCMAKE_SYSTEM_NAME=iOS \
      -DCMAKE_OSX_SYSROOT=iphonesimulator \
      -DCMAKE_OSX_ARCHITECTURES=x86_64 \
      -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
    
    cmake --build "$SCRIPT_DIR/build_sim_x86" --target core_c_lib --config Release > /dev/null 2>&1
    echo "✅ iOS Simulators built successfully!"
    
    # 5. Merge simulator architectures and prepare environments
    echo "🔍 Processing and merging simulator architectures..."
    mkdir -p "$SCRIPT_DIR/build_xcf/products/ios" \
             "$SCRIPT_DIR/build_xcf/products/sim" \
             "$SCRIPT_DIR/build_xcf/headers"
    
    # With "Unix Makefiles", CMake outputs .a files EXACTLY at the root of each build folder.
    LIB_IOS="$SCRIPT_DIR/build_ios/libcore_c_lib.a"
    LIB_SIM_ARM="$SCRIPT_DIR/build_sim_arm/libcore_c_lib.a"
    LIB_SIM_X86="$SCRIPT_DIR/build_sim_x86/libcore_c_lib.a"
    
    # Ensure physical device library is copied
    cp "$LIB_IOS" "$SCRIPT_DIR/build_xcf/products/ios/libcore_c_lib.a"
    
    # Create the universal binary for the simulator
    lipo -create "$LIB_SIM_ARM" "$LIB_SIM_X86" -output "$SCRIPT_DIR/build_xcf/products/sim/libcore_c_lib.a"
    
    # Copy the public header
    cp "$SCRIPT_DIR/core_component.h" "$SCRIPT_DIR/build_xcf/headers/"
    
    # Crear el mapa de módulo para que Swift reconozca la librería C
    cat << 'EOF' > "$SCRIPT_DIR/build_xcf/headers/module.modulemap"
    module CoreC {
        header "core_component.h"
        export *
    }
    EOF
    
    # 6. Create the temporary XCFramework in the build directory
    echo "🚀 Packaging into CoreC.xcframework..."
    xcodebuild -create-xcframework \
      -library "$SCRIPT_DIR/build_xcf/products/ios/libcore_c_lib.a" -headers "$SCRIPT_DIR/build_xcf/headers" \
      -library "$SCRIPT_DIR/build_xcf/products/sim/libcore_c_lib.a" -headers "$SCRIPT_DIR/build_xcf/headers" \
      -output "$SCRIPT_DIR/build_xcf/CoreC.xcframework"
    
    # 7. Organize the definitive isolated SPM environment (Isolate from source code)
    echo "📦 Organizing isolated Swift Package Manager environment..."
    SPM_DIR="$SCRIPT_DIR/build_xcf/spm"
    mkdir -p "$SPM_DIR"
    
    # Move the generated .xcframework into the spm directory
    mv "$SCRIPT_DIR/build_xcf/CoreC.xcframework" "$SPM_DIR/"
    
    # Dynamically generate the Package.swift DIRECTLY inside the spm directory
    cat << 'EOF' > "$SPM_DIR/Package.swift"
    // swift-tools-version: 5.10
    import PackageDescription
    
    let package = Package(
        name: "CoreC",
        products: [
            .library(name: "CoreC", targets: ["CoreC"])
        ],
        targets: [
            .binaryTarget(
                name: "CoreC",
                path: "CoreC.xcframework" // Path relative to Package.swift
            )
        ]
    )
    EOF
    
    echo "🎉 ABSOLUTE VICTORY! Your isolated SPM is ready at: $SPM_DIR"

    This Bash script automates the compilation and packaging of a native C library (core_c_lib) into an isolated Swift Package Manager (SPM) dependency for Apple platforms. After establishing its execution directory and clearing out old build artifacts, the script uses CMake to compile the source code into static libraries (.a) across three distinct target architectures. It generates binaries for physical iOS devices (arm64), M-series Mac iOS Simulators (arm64), and Intel-based Mac iOS Simulators (x86_64).

    Once the compilation is complete, the script merges the two simulator binaries into a single universal library using the lipo tool and groups it alongside the physical device library. Crucially, it injects a custom module.modulemap file next to the public C header file to ensure Swift can seamlessly read the native C interfaces. Finally, it uses xcodebuild to package these components into a unified CoreC.xcframework bundle, places it into a dedicated folder, and automatically writes a Package.swift manifest file, delivering a turnkey binary Swift package ready to be imported into any Xcode project.

    SPM integration into an iOS App

    The final stage involves integrating the newly minted Swift Package into a production environment. To demonstrate this, we will initialize a clean iOS Application project and import the local SPM dependency we just generated.

    Be sure that in target appears the new imported framework:

    Next step we will create a wrapper for the SPM package:

    import Foundation
    import CoreC
    
    struct CoreCWrapper {
    
        
        /// Safe Swift wrapper for the ANSI C function 'getVersion'
            /// Renamed to 'fetchVersion()' to avoid shadowing the global C function name.
            static func fetchVersion() -> String {
                // 1. Allocate a byte array with enough space for an "X.Y.Z" string
                let bufferSize = 32
                var outputBuffer = [Int8](repeating: 0, count: bufferSize)
                
                // 2. Call the global ANSI C function safely without naming conflicts
                let statusCode = getVersion(&outputBuffer, bufferSize)
                
                // 3. Evaluate the status code matching the C macro logic
                switch statusCode {
                case 0: // VERSION_SUCCESS
                    if let versionSwiftString = String(cString: outputBuffer, encoding: .utf8) {
                        return versionSwiftString
                    } else {
                        return "Error: Could not decode version string from C."
                    }
                case -1: // VERSION_ERR_INSUFFICIENT_BUF
                    return "Error from C: Insufficient buffer size."
                default:
                    return "Unknown error in native getVersion component."
                }
            }
    }

    This wrapper approach is highly valuable because it abstracts the low-level, error-prone complexities of inter-language communication, presenting a clean, «Swifty» API to the rest of the application. Instead of forcing consumer code to manually manage C-style memory allocations ([Int8] buffers), handle unsafe pointers, or decipher cryptic integer status codes (like 0 or -1), the wrapper centralizes this dangerous boilerplate in one isolated place. By safely evaluating the execution status and converting raw C-strings into native Swift String types, it guarantees type-safety and robust error handling at the boundary, ensuring that the main application remains idiomatic, safe, and entirely decoupled from the underlying C implementation details.

    Finally use the wrapper:

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                    Text("Version: \(CoreCWrapper.fetchVersion())")
                    .font(.title)
                    .bold()
            }
            .padding()
        }
    }

    Deploy in the simulator for watching results:

    Conclusions

    In this opening installment, we explored the complete lifecycle of integrating an isolated, native C component directly into an iOS application using modern Swift Package Manager workflows. However, this is only the first step toward building a truly unified, multi-platform ecosystem. In the upcoming parts of this series, we will leverage this exact same C core across entirely different environments—reusing the component to power a native Android application and embedding it inside a containerized Vapor Backend server running on Docker.

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

  • Customized TabBar in SwiftUI

    Customized TabBar in SwiftUI

    Building a tab bar in SwiftUI often starts with the simplicity of TabView, but real-world apps quickly demand more flexibility than the default component provides. In this post, we’ll take a practical, step-by-step journey from the standard implementation to a fully customized, animated tab bar—exploring how to progressively enhance styling, restructure layout, and ultimately replace built-in behavior with a state-driven solution. Along the way, we’ll touch on core SwiftUI concepts like data flow, view composition, and animation, using a familiar UI pattern to bridge the gap between basic usage and building polished, production-ready interfaces.

    Basic SwiftUI TabBar

    The minimum code for implementening a TabBar is just following:

    struct ContentView: View {
        var body: some View {
            TabView {
                MainScrollStoryView()
                    .tabItem {
                        Label("Home", systemImage: "doc.text.fill")
                    }
                
                ProfileView()
                    .tabItem {
                        Label("Profile", systemImage: "person.circle.fill")
                    }
    
                SettingsView()
                    .tabItem {
                        Label("Settings", systemImage: "gearshape.fill")
                    }
            }
            .accentColor(.blue)
        }
    }

    It defines a tab-based interface in SwiftUI where a TabView presents three sections—Home, Profile, and Settings—each linked to its own view (MainScrollStoryView, ProfileView, and SettingsView); every tab is configured with a Label combining a title and an SF Symbol icon, and the .accentColor(.blue) modifier sets the active tab’s tint color to blue, so when users tap a tab, the corresponding view is displayed while the selected tab is visually highlighted.

    This is one of the views attached to one of the tabs:

     

    struct MainScrollStoryView: View {
        var body: some View {
            NavigationStack {
                ScrollView {
                    VStack(alignment: .leading, spacing: 25) {
                        Text("The Future of SwiftUI")
                            .font(.system(size: 34, weight: .bold, design: .rounded))
                        ForEach(1...25, id: \.self) { index in
                            VStack(alignment: .leading, spacing: 10) {
                                Text("Chapter \(index)")
                                    .font(.headline)
                                    .foregroundColor(.secondary)
                                
                                Text("This is a demonstration of a ScrollView inside a TabView. In SwiftUI, the TabBar remains anchored at the bottom while the content flows behind it. This specific block of text is part of iteration number \(index), ensuring that we have enough height to trigger the scrolling physics of the device.")
                                    .font(.body)
                                    .lineSpacing(6)
                            }
                            Divider()
                        }
                    }
                    .padding()
                }
                .navigationTitle("Main Feed")
            }
        }
    }

    As in UIKit, there’s no issue embedding one container controller (e.g., a NavigationStack) inside another container controller (such as a TabBar). And finally the result:

     

    Adapt TabBar to your System Dessign

    It’s quite common, especially on projects of a certain scale, to work with a UX/UI team that defines and governs the application’s visual design and user experience. In most of the cases they respect HIG (Human Interface Guidelines), so is just customize basically icons, fonts and colors.

    struct CustomStyledTabView: View {
        init() {
            let appearance = UITabBarAppearance()
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = .systemBackground
            
            let selectedAttributes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.systemPurple,
                .font: UIFont.boldSystemFont(ofSize: 12)
            ]
            
            let normalAttributes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.gray
            ]
            
            appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
            appearance.stackedLayoutAppearance.selected.iconColor = .systemPurple
            
            appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
            appearance.stackedLayoutAppearance.normal.iconColor = .gray
    
            UITabBar.appearance().standardAppearance = appearance
            UITabBar.appearance().scrollEdgeAppearance = appearance
        }
    
        var body: some View {
            TabView {
                MainScrollStoryView()
                    .tabItem {
                        Label("Home", systemImage: "doc.text.fill")
                    }
                ProfileView()
                    .tabItem {
                        Label("Profile", systemImage: "person.circle.fill")
                    }
                
                SettingsView()
                    .tabItem {
                        Label("Settings", systemImage: "gearshape.fill")
                    }
            }
        }
    } 

    This SwiftUI View struct customizes the global appearance of all tab bars in the app by configuring a UITabBarAppearance instance during initialization. It sets an opaque background using the system background color, then defines distinct text and icon styles for selected and unselected tab items—selected items appear in purple with a bold font, while unselected ones are gray. These styling attributes are applied specifically to the standard stacked layout used in bottom tab bars. Finally, the configured appearance is assigned to both the standardAppearance and scrollEdgeAppearance of UITabBar via the UIAppearance proxy, ensuring consistent styling across the entire app.

    Fully customization

    Sometimes the default customization options provided by the SDK aren’t sufficient, and you need to push beyond them—for example, when you want to introduce additional animations.

    struct FloatingTabBarView: View {
        @State private var selectedTab = 0
        @State private var isTabBarVisible = true
        
        var body: some View {
            ZStack(alignment: .bottom) {
                NavigationStack {
                    MainScrollStoryView()
                    .navigationTitle("Auto-hide Bar")
                    .onScrollGeometryChange(for: CGFloat.self) { geo in
                        geo.contentOffset.y
                    } action: { oldValue, newValue in
                        let scrollingDown = newValue > oldValue
                        let isAtTop = newValue <= 0
                        
                        withAnimation(.easeInOut(duration: 0.3)) {
                            if isAtTop {
                                isTabBarVisible = true
                            } else if scrollingDown {
                                isTabBarVisible = false
                            } else {
                                isTabBarVisible = true
                            }
                        }
                    }
                }
                if isTabBarVisible {
                    CustomBar(selectedTab: $selectedTab)
                        .transition(.move(edge: .bottom).combined(with: .opacity))
                        .zIndex(1)
                }
            }
        }
    }
    
    struct CustomBar: View {
        @Binding var selectedTab: Int
        
        var body: some View {
            HStack(spacing: 40) {
                TabBarButton(index: 0, icon: "house.fill", selectedTab: $selectedTab)
                TabBarButton(index: 1, icon: "magnifyingglass", selectedTab: $selectedTab)
                TabBarButton(index: 2, icon: "person.fill", selectedTab: $selectedTab)
            }
            .padding(.horizontal, 30)
            .padding(.vertical, 15)
            .background(.ultraThinMaterial)
            .clipShape(Capsule())
            .shadow(color: .black.opacity(0.15), radius: 10, y: 5)
            .padding(.bottom, 20)
        }
    }
    
    struct TabBarButton: View {
        let index: Int
        let icon: String
        @Binding var selectedTab: Int
        
        var body: some View {
            Button(action: { selectedTab = index }) {
                Image(systemName: icon)
                    .font(.system(size: 22))
                    .foregroundColor(selectedTab == index ? .blue : .gray)
            }
        }
    }

    This code defines a custom SwiftUI tab bar system that overlays a floating, animated tab bar on top of a scrollable view. The main FloatingTabBarView uses a ZStack to position content and the tab bar, embedding a NavigationStack with a scrollable MainScrollStoryView. It listens to scroll position changes via onScrollGeometryChange, comparing the previous and current vertical offsets to determine scroll direction. Based on this, it toggles the isTabBarVisible state with a smooth animation: the tab bar hides when the user scrolls down, reappears when scrolling up, and stays visible when the content is at the top.

    The CustomBar and TabBarButton components define the visual and interactive behavior of the floating tab bar. CustomBar lays out three tab buttons horizontally inside a capsule-shaped container with a blurred background (ultraThinMaterial) and shadow, giving it a modern floating appearance. Each TabBarButton updates the shared selectedTab binding when tapped and visually reflects its active state by switching icon color between blue (selected) and gray (unselected). The tab bar itself animates in and out using a combined slide-from-bottom and fade transition, creating a polished, dynamic UI effect.

    func combineUsage() {
        NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            .sink { _ in
                print("App active")
            }
            .store(in: &cancellables)
    }
    

    Conclusions

    Across the three examples, we’ve explored how to implement a TabBar, apply basic customization, and extend its behavior beyond the standard capabilities.

    You can find the source code for this example in the following GitHub repository.

    References

  • Daily Swift Combine by example

    Daily Swift Combine by example

    The aim of this post is to get introduced to Combine by comparing it with regular operations that we perform on a daily basis while programming in Swift. It focuses on illustrating how common imperative patterns can be re-expressed using a reactive and declarative approach.

    From Callback to Combine

    Using a traditional completion handler is straightforward and lightweight. It works well for single, isolated asynchronous operations where you just need to return a Result once and move on. It introduces no dependency on Combine and keeps the control flow explicit and easy to follow. However, composition becomes cumbersome as soon as you need to chain multiple async steps, handle retries, debounce input, combine multiple data sources, or coordinate cancellation. Cancellation and flow control must be designed manually, and complex logic can quickly devolve into nested closures or scattered error handling.

    struct User: Decodable {
        let id: Int
        let name: String
    }
    
    final class API {
        func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
                completion(.success(User(id: id, name: "Ada")))
            }
        }
    }

    Wrapping the same API in a Combine Future turns the operation into a Publisher, which enables declarative composition with operators like map, flatMap, retry, and catch. This makes it much easier to build scalable, testable pipelines and maintain architectural consistency (e.g., in MVVM). The trade-offs are additional conceptual complexity, lifecycle management via AnyCancellable, and the fact that Future is eager and single-shot. Moreover, cancellation of a subscription does not automatically cancel the underlying work unless the original API supports it and you explicitly propagate that behavior.

    import Combine
    
    final class APICombine {
        private let api = API()
    
        func fetchUser(id: Int) -> AnyPublisher<User, Error> {
            Future { [api] promise in
                api.fetchUser(id: id) { result in
                    promise(result)
                }
            }
            .eraseToAnyPublisher()
        }
    }
    
    var cancellables = Set<AnyCancellable>()
    
    func fetchUser(id: Int) {
        let api = APICombine()
        api.fetchUser(id: 1)
            .sink(
                receiveCompletion: { completion in
                    if case let .failure(error) = completion {
                        print("Error:", error)
                    }
                },
                receiveValue: { user in
                    print("User:", user.name)
                }
            )
            .store(in: &cancellables)
    }

    Real Networking: URLSession.dataTaskPublisher

    Using URLSession.dataTaskPublisher provides a highly declarative and composable approach to networking. It integrates natively into the Combine pipeline, allowing you to chain operators such as map, tryMap, decode, retry, catch, and receive(on:) in a single, expressive stream. This makes transformation, error propagation, threading, and cancellation first-class concerns. It also improves testability when paired with dependency injection and custom URLSession configurations, and it aligns naturally with reactive UI layers (e.g., SwiftUI with @Published). The main advantages are composability, consistency with reactive architecture, built-in cancellation via AnyCancellable, and clearer data-flow semantics.

    import Foundation
    import Combine
    
    struct Post: Decodable {
        let id: Int
        let title: String
    }
    
    enum APIError: Error {
        case badStatus(Int)
    }
    
    final class PostsService {
        func fetchPosts() -> AnyPublisher<[Post], Error> {
            let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    
            return URLSession.shared.dataTaskPublisher(for: url)
                .tryMap { output -> Data in
                    if let http = output.response as? HTTPURLResponse,
                       !(200...299).contains(http.statusCode) {
                        throw APIError.badStatus(http.statusCode)
                    }
                    return output.data
                }
                .decode(type: [Post].self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        }
    }

    However, dataTaskPublisher introduces additional abstraction and complexity compared to a traditional completion-handler approach. The generic types in Combine pipelines can become difficult to read, often requiring eraseToAnyPublisher() to manage API surface complexity. Debugging reactive chains can also be more challenging due to operator layering and asynchronous propagation. Moreover, for simple one-off requests, a completion handler or even Swift’s modern async/await syntax may be more readable and straightforward. Combine shines in scenarios involving multiple asynchronous streams, transformation pipelines, or UI bindings, but it may feel unnecessarily heavy for basic networking tasks.

    let service = PostsService()
    
    func PostServiceUsage() {
    
        service.fetchPosts()
            .receive(on: DispatchQueue.main) // UI updates
            .sink(
                receiveCompletion: { print("Completion:", $0) },
                receiveValue: { posts in print("Posts:", posts.count) }
            )
            .store(in: &cancellables)
    }

    NotificationCenter in Combine

    Using NotificationCenter with the classic addObserver API is straightforward and has minimal abstraction overhead. It works well for simple, fire-and-forget events and does not require knowledge of reactive programming. However, it is loosely typed (the notification’s payload is typically extracted from userInfo), which increases the risk of runtime errors. It also requires manual lifecycle management: you must ensure observers are removed appropriately (unless relying on the block-based API introduced in iOS 9+), and there is no built-in way to declaratively transform, filter, or compose events. As the number of notifications grows, code can become fragmented and harder to reason about.

    import Foundation
    import UIKit
    
    func classicUsage() {
        NotificationCenter.default.addObserver(
            forName: UIApplication.didBecomeActiveNotification,
            object: nil,
            queue: .main
        ) { _ in
            print("App activa")
        }
    }

    The Combine-based NotificationCenter.Publisher approach integrates notifications into a reactive stream, enabling strong composition through operators like map, filter, debounce, and merge. This allows you to declaratively transform and coordinate notification-driven events within a unified asynchronous pipeline. Memory management is also more explicit and predictable via AnyCancellable, and cancellation semantics are clearer. The trade-offs are increased conceptual complexity and a steeper learning curve, particularly for teams unfamiliar with reactive paradigms. Additionally, for very simple use cases, Combine may introduce unnecessary abstraction compared to the traditional observer pattern.

    func combineUsage() {
        NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            .sink { _ in
                print("App active")
            }
            .store(in: &cancellables)
    }
    

    Timer as a Publlisher

    Using Timer.publish(every:on:in:) from Combine provides a declarative, composable approach to time-based events. Instead of imperatively scheduling a Timer and manually invalidating it, you model time as a stream of values that can be transformed with operators like map, scan, throttle, or debounce. This makes it especially powerful when the timer is just one part of a larger reactive pipeline (e.g., polling a network endpoint, driving UI state, or coordinating multiple asynchronous streams). Memory management is also more uniform: cancellation is handled via AnyCancellable, which integrates naturally with other Combine subscriptions. The main advantage is composability and consistency within a reactive architecture.

    import Combine
    
    func setupTimer() {
        Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .scan(0) { count, _ in count + 1 }  // contador
            .sink { value in
                print("Segundos:", value)
            }
            .store(in: &cancellables)
    }

    In contrast, the traditional Timer.scheduledTimer approach is simpler and often more readable for isolated, imperative use cases—particularly when you just need a repeating callback with minimal transformation logic. It has less conceptual overhead and avoids introducing reactive abstractions where they may not be justified. However, it requires manual lifecycle management (invalidating the timer at the correct moment), is less expressive for chaining asynchronous behavior, and does not integrate naturally with other reactive data flows. Therefore, the imperative timer is often preferable for small, self-contained tasks, while the Combine publisher approach scales better in complex, event-driven architectures.

    Search with debounce

    Using a reactive approach with Combine (e.g., debounce, removeDuplicates, flatMap) centralizes the entire search pipeline into a single declarative data flow. The main advantage is composability: input normalization, rate limiting, cancellation of in-flight requests, error handling, and threading can all be expressed as operators in a predictable chain. This significantly reduces race conditions and makes it easier to reason about asynchronous behavior. Additionally, flatMap combined with switchToLatest (if used) allows automatic cancellation of outdated requests, which is critical in fast-typing scenarios. The architecture also scales well, especially in MVVM, because the ViewModel cleanly separates input streams from output state.

    import Combine
    import UIKit
    
    final class SearchViewModel {
    
        let query = PassthroughSubject<String, Never>()
    
        @Published private(set) var results: [String] = []
    
        private var cancellables = Set<AnyCancellable>()
    
        init() {
            query
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .removeDuplicates()
                .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
                .filter { !$0.isEmpty }
                .flatMap { q in
                    Self.searchPublisher(query: q)
                        .catch { _ in Just([]) }
                }
                .assign(to: &$results)
        }
    
        private static func searchPublisher(query: String) -> AnyPublisher<[String], Error> {
            Future { promise in
                DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
                    promise(.success(["\(query) 1", "\(query) 2", "\(query) 3"]))
                }
            }
            .eraseToAnyPublisher()
        }
    }

    The main drawback is cognitive overhead and complexity. Combine introduces advanced abstractions (Publishers, Subscribers, Subjects, operator chains) that can be harder to debug and understand compared to imperative callbacks or simple target–action patterns. Error propagation and type signatures can become verbose, and misuse of operators (e.g., forgetting to manage cancellation or incorrectly handling threading) may introduce subtle bugs. For small or straightforward search implementations, a manual debounce using DispatchWorkItem or Timer can be simpler and easier to maintain, particularly for teams not already comfortable with reactive programming paradigms.

    final class SearchVC: UIViewController {
        private let vm = SearchViewModel()
        private var cancellables = Set<AnyCancellable>()
        private let textField = UITextField()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
    
            vm.$results
                .receive(on: DispatchQueue.main)
                .sink { results in
                    print("Resultados:", results)
                }
                .store(in: &cancellables)
        }
    
        @objc private func textChanged() {
            vm.query.send(textField.text ?? "")
        }
    }

    Conclusions

    With the following examples, I did not intend to directly replace traditional development patterns with Combine, but rather to identify development scenarios where the Combine framework is an appropriate and valuable option.

    You can find the source code for this example in the following GitHub repository.

    References

    • Combine

      Apple Developer Documentation

  • Different Aproximations with MVVM

    Different Aproximations with MVVM

    Implement MVVM using UIKit, RxSwift, Combine, and SwiftUI is interesting because it shows how a single architectural pattern adapts across very different UI and reactive paradigms. By comparing these approaches, readers can clearly distinguish MVVM’s core responsibilities.

    This perspective reflects the real evolution of iOS codebases, helps developers reason about architectural trade-offs, and demonstrates that MVVM is a long-lasting design mindset rather than a framework-dependent trend.

    MVVM Pattern

    MVVM splits your app into three layers so UI code stays simple and testable:

    • Model

      • Your domain data + business rules (e.g., User, Order, validation, persistence entities).

      • No UI knowledge.

    • View

      • The UI layer (SwiftUI View or UIKit UIViewController/UIView).

      • Displays state and forwards user events (tap, refresh, text input).

      • Should contain minimal logic—mostly layout + binding.

    • ViewModel

      • The “presentation logic” layer.

      • Transforms Models into UI-ready state (strings, sections, flags like isLoading, errorMessage).

      • Handles user intents (e.g., loginTapped(), load()), calls services, updates state.

    Class diagram

    Observing the class diagram is easier to see a possile retain cycle can occur between the View and the ViewModel, but only if they reference each other strongly. This typically happens in UIKit when the ViewModel exposes closures (e.g., callbacks for UI updates) and the View captures self strongly inside those closures; the View holds the ViewModel, and the ViewModel indirectly holds the View through the closure. To avoid this, capture self weakly ([weak self]) in closures. In SwiftUI, retain cycles are much less common because the View is a value type (struct) and the framework manages the lifecycle, but you still need to be careful with long-lived objects, timers, or async tasks inside the ViewModel that capture references strongly.

    UIKit

    UIKit UIViewController displays a list of users in a table view using the MVVM pattern: the controller owns a UserListViewModel, sets up the UI and table view configuration in viewDidLoad, and establishes a binding via a closure (onDataUpdate) so that when the ViewModel finishes fetching users and updates its data, the table view is reloaded. The closure captures self weakly to avoid a retain cycle between the view controller and the ViewModel, and the actual data-fetching logic is delegated to the ViewModel through fetchUsers(), keeping presentation logic separated from UI concerns.

    class UserListViewController: UIViewController {
        
        private let tableView = UITableView()
        private let viewModel = UserListViewModel()
        private let cellId = "UserCell"
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
            setupBindings()
            viewModel.fetchUsers()
        }
        
        private func setupBindings() {
            viewModel.onDataUpdate = { [weak self] in
                self?.tableView.reloadData()
            }
        }
        
        private func setupUI() {
            title = "Usuarios"
            view.addSubview(tableView)
            tableView.frame = view.bounds
    
            tableView.dataSource = self
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
        }
    }
    
    // MARK: - TableView DataSource
    extension UserListViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return viewModel.numberOfUsers
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
            let user = viewModel.user(at: indexPath.row)
            
            // Configuramos la celda usando los datos procesados por el ViewModel
            var content = cell.defaultContentConfiguration()
            content.text = user.name
            content.secondaryText = user.email
            cell.contentConfiguration = content
            
            return cell
        }
    }

    The ViewModel for an MVVM architecture that manages a private list of User models and exposes table-friendly accessors (numberOfUsers and user(at:)) to the View, while using a closure (onDataUpdate) as a simple binding mechanism to notify the View when data changes. The fetchUsers() method simulates an asynchronous API call, updates the internal user list after a delay, and triggers the callback so the UI (such as a table view) can refresh without the ViewModel depending on any UIKit components.

     

    import Foundation
    
    class UserListViewModel {
        private var users: [User] = []
        
        var onDataUpdate: (() -> Void)?
    
        var numberOfUsers: Int {
            return users.count
        }
        
        func user(at index: Int) -> User {
            return users[index]
        }
        
        func fetchUsers() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                self.users = [
                    User(name: "Alice", email: "alice@example.com"),
                    User(name: "Bob", email: "bob@example.com"),
                    User(name: "Charlie", email: "charlie@example.com")
                ]
                self.onDataUpdate?()
            }
        }
    }

    RxSwift

    RxSwift is a reactive framework for Swift that represents asynchronous events as observable streams, enabling a declarative and composable way to handle data flow, UI updates, and state changes without relying on callbacks or delegates.

    RxSwift is a 3rd party and first step is include it in the project:

    With plain UIKit, MVVM uses imperative bindings like closures or delegates to update the View, while with RxSwift it uses declarative reactive streams that automatically propagate changes, resulting in cleaner data flow but more abstraction.

    This code defines a UIKit UIViewController that uses RxSwift and RxCocoa to apply the MVVM pattern in a reactive style. The view controller displays a table view and connects it to a stream of User data coming from the ViewModel, so the table updates automatically whenever the data changes. It also listens for row selection events to respond to user actions, manages memory using a DisposeBag, and keeps the controller focused on UI setup while data updates and events are handled through reactive bindings instead of manual callbacks.

    import UIKit
    import RxSwift
    import RxCocoa
    
    class UserListViewController: UIViewController {
        
        private let tableView = UITableView()
        private let viewModel = UserListViewModel()
        private let disposeBag = DisposeBag()
        private let cellId = "UserCell"
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
            setupBindings()
            viewModel.fetchUsers()
        }
        
        private func setupBindings() {
            viewModel.users
                .bind(to: tableView.rx.items(cellIdentifier: cellId, cellType: UITableViewCell.self)) { (row, user, cell) in
                    var content = cell.defaultContentConfiguration()
                    content.text = user.name
                    content.secondaryText = user.email
                    cell.contentConfiguration = content
                }
                .disposed(by: disposeBag)
            
            tableView.rx.modelSelected(User.self)
                .subscribe(onNext: { user in
                    print("Usuario seleccionado: \(user.name)")
                })
                .disposed(by: disposeBag)
        }
        
        private func setupUI() {
            title = "Usuarios Rx"
            view.addSubview(tableView)
            tableView.frame = view.bounds
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
        }
    }

    ViewModel using RxSwift that manages and exposes a list of User models as a reactive data stream: it stores the users in a BehaviorRelay, which always holds the latest value, exposes it to the View as a read-only Observable, and updates the stream in fetchUsers() by accepting new data, causing any subscribed Views (such as a table view) to automatically receive the updated user list and refresh without manual callbacks or delegates.

    import RxSwift
    import RxRelay
    
    class UserListViewModel {
    
        private let usersRelay = BehaviorRelay<[User]>(value: [])
        
        var users: Observable<[User]> {
            return usersRelay.asObservable()
        }
        
        func fetchUsers() {
            let mockData = [
                User(name: "Alice", email: "alice@example.com"),
                User(name: "Bob", email: "bob@example.com"),
                User(name: "Charlie", email: "charlie@example.com")
            ]
            usersRelay.accept(mockData)
        }
    }

    Combine

    Combine is Apple’s native reactive framework for handling asynchronous data with publishers and subscribers. Compared to RxSwift in MVVM, Combine is more tightly integrated with Swift and SwiftUI and uses less boilerplate, while RxSwift is more mature, feature-rich, and cross-platform but requires a third-party dependency.

    This code defines a UIKit UIViewController that uses Combine to implement MVVM-style data binding: it observes the @Published users property exposed by the ViewModel via the $users publisher, ensures updates are delivered on the main thread, and reloads the table view automatically whenever the user list changes, while managing the subscription lifecycle with a set of AnyCancellable objects and keeping the ViewController focused on UI setup and rendering rather than data-fetching logic.

    import UIKit
    import Combine
    
    class UserListViewController: UIViewController {
        
        private let tableView = UITableView()
        private let viewModel = UserListViewModel()
        private let cellId = "UserCell"
        
        private var cancellables = Set<AnyCancellable>()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
            setupBindings()
            viewModel.fetchUsers()
        }
        
        private func setupBindings() {
            viewModel.$users
                .receive(on: RunLoop.main)
                .sink { [weak self] _ in
                    self?.tableView.reloadData()
                }
                .store(in: &cancellables)
        }
        
        private func setupUI() {
            title = "Usuarios con Combine"
            view.addSubview(tableView)
            tableView.frame = view.bounds
            tableView.dataSource = self
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
        }
    }

    This code defines a ViewModel using Combine that manages a list of User models for an MVVM architecture: it exposes the users array as a read-only @Published property so any subscribers are automatically notified when it changes, and simulates an asynchronous data fetch in fetchUsers() by updating the users after a delay on the main thread, which triggers Combine’s publisher to emit a new value and allows bound Views to refresh their UI reactively.

    import Foundation
    import Combine
    
    class UserListViewModel {
        @Published private(set) var users: [User] = []
        
        func fetchUsers() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                self.users = [
                    User(name: "Ana Combine", email: "ana@apple.com"),
                    User(name: "Pedro Publisher", email: "pedro@apple.com"),
                    User(name: "Sara Subscriber", email: "sara@apple.com")
                ]
            }
        }
    }

    SwiftUI

    SwiftUI is naturally aligned with the MVVM pattern because its declarative UI model is designed to observe and react to state changes exposed by ViewModels: the View is a lightweight struct that binds to an ObservableObject ViewModel, the ViewModel uses properties like @Published to expose state derived from Models, and SwiftUI automatically updates the UI whenever that state changes. As a result, MVVM in SwiftUI requires very little glue code, encourages clear separation of concerns, and makes reactive data flow the default rather than an added architectural layer.

    This SwiftUI view displays a list of users using the MVVM pattern by owning a @StateObject UserListViewModel, conditionally rendering a loading indicator while data is being fetched and a List of user details once loading completes, and triggering the data load in onAppear; SwiftUI automatically re-renders the UI whenever the ViewModel’s published state (users or isLoading) changes, keeping the view declarative and free of data-fetching or presentation logic.

    import SwiftUI
    
    struct UserListView: View {
        @StateObject private var viewModel = UserListViewModel()
        
        var body: some View {
            NavigationView {
                Group {
                    if viewModel.isLoading {
                        ProgressView("Loading users...")
                    } else {
                        List(viewModel.users) { user in
                            VStack(alignment: .leading) {
                                Text(user.name)
                                    .font(.headline)
                                Text(user.email)
                                    .font(.subheadline)
                                    .foregroundColor(.secondary)
                            }
                        }
                    }
                }
                .navigationTitle("Users")
                .onAppear {
                    viewModel.fetchUsers()
                }
            }
        }
    }

    This code defines a SwiftUI-compatible ViewModel that conforms to ObservableObject and manages user list state for an MVVM architecture: it exposes the users array and a loading flag as @Published properties so SwiftUI views automatically react to changes, sets isLoading to true when a simulated asynchronous fetch starts, updates the users after a delay on the main thread, and then resets the loading state, enabling the UI to seamlessly switch between loading and content states.

    import Foundation
    import Combine
    
    class UserListViewModel: ObservableObject {
        @Published var users: [User] = []
        @Published var isLoading = false
        
        func fetchUsers() {
            isLoading = true
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                self.users = [
                    User(name: "Ana SwiftUI", email: "ana@apple.com"),
                    User(name: "Pedro State", email: "pedro@apple.com"),
                    User(name: "Sara Binding", email: "sara@apple.com")
                ]
                self.isLoading = false
            }
        }
    }

    Conclusions

    MVVM is a pattern that has been present since the early days of Swift and has been easily adopted across different eras of iOS development, from UIKit to SwiftUI, including reactive approaches with RxSwift and Combine.

    You can find the source code for this example in the followingGitHub repository. There is a commit per approach.

    References

  • Maximizing enum in Swift Development

    Maximizing enum in Swift Development

    Maximizing enum usage in Swift is compelling because it moves beyond treating them as simple lists of constants and showcases their power as first-class citizens. This approach not only makes a codebase more expressive and readable but also teaches developers how to leverage Swift’s robust type system to model complex business logic with minimal overhead.

    default and @unknown default

    To set the stage, let’s warm up by distinguishing between default and @unknown default. While both serve as a safety net for unlisted cases in a switch statement, they play very different roles.

    The default Case

    The default keyword is a «catch-all» that silences the compiler’s requirement for exhaustiveness. It tells Swift: «I don’t care what else might be in this enum; treat everything else the same way.»

    • When to use it: Use default when you genuinely want to group a large number of existing cases together, or when you are switching over types that aren’t enums (like Strings or Ints).

    • The Risk: If you add a new case to your enum later, the compiler will not warn you. The new case will simply fall into the default block, which can lead to «silent» logic bugs.

    enum UserRole {
        case admin, editor, viewer, guest
    }
    
    let role = UserRole.guest
    
    switch role {
    case .admin:
        print("Full Access")
    default: 
        // Covers editor, viewer, and guest identically
        print("Limited Access")
    }
    @unknown

    @unknown default is a «cautionary» catch-all. It handles any cases that aren’t explicitly defined, but it triggers a compiler warning if you haven’t accounted for all known cases.

    • When to use it: Use it when dealing with enums that might change in the future (especially those from Apple’s frameworks like UNAuthorizationStatus).

    • The Benefit: It provides the best of both worlds: your code still compiles if a new case is added (preventing a crash), but the compiler warns you that you need to go back and handle that specific new case properly.

    import NotificationCenter
    
    let status: UNAuthorizationStatus = .authorized
    
    switch status {
    case .authorized:
        print("Authorized")
    case .denied:
        print("Denied")
    case .notDetermined:
        print("Not Determined")
    case .provisional, .ephemeral:
        print("Trial/Limited")
    @unknown default:
        // If Apple adds a new status in iOS 20, this code still runs,
        // but the compiler will show a warning saying: 
        // "Switch implies matching of 'newFutureCase'..."
        print("Handle unknown future state")
    }

    Beyond Swift.Result switching…

    While Swift.Result is typically handled by switching over basic .success and .failure cases, we can unlock more power by inspecting the data within those cases. In the following example, we take it a step further: we handle the .failure state while splitting the .success state into two distinct logic paths—one for a populated list of items and another for when no data is received.

    Here is how you can implement this using pattern matching to keep your code clean and expressive:

    public func processFeedRequest() {
        fetchFeed() { result in
            switch result {
            case .success(.some(let feedResponse)):
                print("Feed Response: \(String(describing: feedResponse))")
            case .success(.none):
                print("No feed response")
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    }

    In Swift, an Optional is actually an enum under the hood! When you write FeedResponse?, the compiler sees it as Optional<FeedResponse>.

    • .none: This is exactly the same as nil. It represents the absence of a value.

    • .some(let value): This represents the presence of a value. The value is «wrapped» inside the enum, and the let feedResponse syntax «unwraps» it so you can use it directly.

    Switching Tuples

    Actually, you have been able to evaluate tuples in Swift’s switch statements since Swift 1.0, released in 2014.

    Swift was designed from the ground up with Pattern Matching as a core feature. This allows the switch statement to decompose complex data structures, like tuples and enums with associated values, very easily.

    You can combine an enum and another variable into a tuple right inside the switch statement. This is incredibly useful for conditional logic that depends on two different factors.

    Using tuples in your enum logic is a «pro move» because it allows you to avoid nested if statements. Instead of checking the enum and then checking a secondary condition, you can express the entire business rule in a single, readable line.

        func testExample() throws {
            let expectedResult: ResultImageURL = .success(aFeed)
            let exp = expectation(description: "Wait for cache retrieval")
            
            fetchFeed { retrievedResult in
                switch (expectedResult, retrievedResult) {
                case (.success(.none), .success(.none)),
                     (.failure, .failure):
                    break
                    
                case let (.success(.some(expected)), .success(.some(retrieved))):
                    XCTAssertEqual(retrieved.feeds, expected.feeds)
                    XCTAssertEqual(retrieved.timestamp, expected.timestamp)
                    
                default:
                    XCTFail("Expected to retrieve \(expectedResult), got \(retrievedResult) instead")
                }
                exp.fulfill()
            }
            
            wait(for: [exp], timeout: 1.0)
        }

    This asynchronous XCTest verifies that fetchFeed completes with the expected Result by waiting on an expectation, comparing the retrieved result against a predefined expected one, and asserting correct behavior for all valid cases: both being failures, both being successful with no cached value, or both being successful with a cached value whose contents (feeds and timestamp) are validated for equality; any mismatch between expected and retrieved outcomes causes the test to fail explicitly, ensuring both correctness of the async flow and the integrity of the returned data.

    Of course, this function could be refactored into a helper not only to validate the happy-path scenario, but also to cover the empty and error cases as well.

    where clause in case

    In Swift, a where clause in a case branch adds an additional boolean condition that must be satisfied for that pattern to match, allowing you to refine or differentiate the same enum case based on contextual rules (such as values, ranges, or predicates) without introducing nested if statements; this makes the control flow more declarative, improves readability, and keeps the conditional logic tightly coupled to the pattern being matched.

    enum NetworkResult {
        case success(Data, HTTPURLResponse)
        case failure(Error)
    
        var isValid: Bool {
            switch self {
            case let .success(_, response)
                where (200..<300).contains(response.statusCode):
                return true
    
            case let .success(_, response)
                where response.statusCode == 401:
                return false
    
            case .failure:
                return false
    
            default:
                return false
            }
        }
    }

    This code defines an enum NetworkResult with a computed property isValid that uses a switch on self to derive a Boolean value based on both the enum case and associated values: when the result is .success, the switch further refines the match using where clauses to inspect the HTTP status code, returning true for successful 2xx responses and false for an unauthorized 401, while any .failure or other non-matching success cases also return false, making isValid a calculated variable that encapsulates response-validation logic directly within the enum.

    Conclusions

    Switch enum structures in Swift are more than a control-flow construct that selects and executes a specific block of code based on matching a value against a set of predefined patterns or cases. In this post, I have presented a few examples to illustrate that.

    You can find the source code for this example in the following GitHub repository.

    References

  • Stopping DoS Attacks: Vapor + Redis

    Stopping DoS Attacks: Vapor + Redis

    Many iOS apps rely on custom APIs built with Vapor, and when those services become unavailable due to a DoS attack, the user blames the app, not the server. Showing iOS developers how to design safer backend interactions, implement rate-limiting with Redis, and understand threat models helps them build more resilient apps, anticipate real-world traffic spikes, and avoid outages that can ruin user experience.

    In this post, we’ll build a simple Vapor service encapsulated in a Docker container, and then integrate and configure Redis to help protect the service from DoS attacks.

    Hello Vapor Server

    Start by creating a new Vapor project using the following command:

    vapor new VaporDoS

    You will be asked about Fluent and Leaf, but we will not be working with databases (Fluent) or HTML templates (Leaf).

    Screenshot

    Navigate to the newly created folder. In this example, we’ll use Visual Studio Code as our source editor. Let’s begin by defining the dependencies and targets for our Vapor project:

    // swift-tools-version:5.9
    import PackageDescription
    
    let package = Package(
        name: "VaporDockerExample",
        platforms: [
            .macOS(.v13)
        ],
        dependencies: [
            .package(url: "https://github.com/vapor/vapor.git", from: "4.92.0")
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                    .product(name: "Vapor", package: "vapor")
                ],
                path: "Sources/App"
            ),
            .executableTarget(
                name: "Run",
                dependencies: [
                    .target(name: "App")
                ],
                path: "Sources/Run"
            )
        ]
    )

    This Swift Package Manager manifest defines a Vapor-based server project called VaporDockerExample, specifying that it runs on macOS 13 or later and depends on the Vapor framework (version 4.92.0 or newer). It organizes the project into two targets: an App target that contains the application logic and imports the Vapor product, and a Run executable target that depends on the App target and serves as the entry point for launching the server. The file essentially tells SwiftPM how to build, structure, and run the Vapor application.

    Later on, we’ll continue by defining the configuration in configuration.swift:

    import Vapor
    
    public func configure(_ app: Application) throws {
        // Hostname: en Docker debe ser 0.0.0.0
        if let host = Environment.get("HOSTNAME") {
            app.http.server.configuration.hostname = host
        } else {
            app.http.server.configuration.hostname = "0.0.0.0"
        }
    
        // Puerto desde variable de entorno (útil para Docker / cloud)
        if let portEnv = Environment.get("PORT"), let port = Int(portEnv) {
            app.http.server.configuration.port = port
        } else {
            app.http.server.configuration.port = 8080
        }
    
        // Rutas
        try routes(app)
    }

    It configures a Vapor application by setting up its HTTP server hostname and port based on environment variables, which is particularly useful when running inside Docker or cloud environments. It first checks for a HOSTNAME variable to determine the server’s bind address, defaulting to 0.0.0.0 so the service is accessible from outside the container. It then reads the PORT environment variable to set the listening port, falling back to port 8080 if none is provided. Finally, it registers the application’s routes by calling routes(app), completing the server’s basic setup.

    Next, let’s define a new route for our hello endpoint:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            return "Hola desde Vapor dentro de Docker 👋"
        }
    
        app.get { req -> String in
            "Service OK"
        }
    }

    This code defines two HTTP routes for a Vapor application. The first route, accessible at /hello, responds to GET requests with the text “Hola desde Vapor dentro de Docker 👋”. The second route is the root endpoint /, which also responds to GET requests and returns “Service OK”. Together, these routes provide simple test endpoints to verify that the Vapor service is running correctly, especially when deployed inside Docker.

     
    import App
    import Vapor
    
    @main
    struct Run {
        static func main() throws {
            var env = try Environment.detect()
            try LoggingSystem.bootstrap(from: &env)
            let app = Application(env)
            defer { app.shutdown() }
            try configure(app)
            try app.run()
        }
    }

    This code defines the main entry point for a Vapor application, handling its full lifecycle from startup to shutdown. It begins by detecting the current runtime environment (such as development or production), initializes the logging system accordingly, and then creates an Application instance based on that environment. After ensuring the application will shut down cleanly when execution finishes, it calls configure(app) to set up server settings and routes, and finally starts the server with app.run(), making the Vapor service ready to accept incoming requests.

    Once we’ve finished implementing our Vapor code, the next step is to define a Dockerfile that will build and package our application into a lightweight image ready to run in any containerized environment.

    # Phase 1: build
    FROM swift:5.10-jammy AS build
    
    WORKDIR /app
    
    # Copy Package.swift and Source folder
    COPY Package.swift ./
    COPY Sources ./Sources
    
    # (Optional) resolve dependencies before, for cache
    RUN swift package resolve
    
    # Compile in release mode
    RUN swift build -c release --static-swift-stdlib
    
    # Phase 2: runtime
    FROM ubuntu:22.04 AS run
    
    # Minimal runtime dependencies for binary in Swift
    RUN apt-get update && \
        apt-get install -y \
        libbsd0 \
        libcurl4 \
        libxml2 \
        libz3-4 \
        && rm -rf /var/lib/apt/lists/*
    
    WORKDIR /run
    
    COPY --from=build /app/.build/release/Run ./
    
    EXPOSE 8080
    
    # In order vapor listens inside Docker
    ENV PORT=8080
    ENV HOSTNAME=0.0.0.0
    
    CMD ["./Run"]
    

    This Dockerfile builds and packages a Vapor application into a lightweight, production-ready container using a two-phase approach. The first phase uses the official Swift 5.10 image to compile the app in release mode with static Swift libraries, producing a small and efficient binary. It copies the project’s manifest and source code, resolves dependencies, and builds the executable. The second phase creates a minimal Ubuntu 22.04 runtime image, installs only the system libraries required by the Swift binary, and copies in the compiled Run executable from the build stage. It exposes port 8080, sets the necessary environment variables to ensure Vapor listens correctly inside Docker, and finally launches the server.

    This setup isn’t strictly necessary yet, but it prepares the project for a smooth integration of Redis in the next section.

    version: "3.8"
    
    services:
      vapor-app:
        build: .
        container_name: vapor-docker-example
        ports:
          - "8080:8080"
        environment:
          # Must match with what is being used in configure.swift
          PORT: "8080"
          HOSTNAME: "0.0.0.0"
        restart: unless-stopped

    This docker-compose.yml file defines a single service called vapor-app, which builds a Docker image from the current directory and runs the Vapor application inside a container named vapor-docker-example. It maps port 8080 on the host to port 8080 in the container so the Vapor server is accessible externally, and it sets the environment variables PORT and HOSTNAME to ensure the app listens correctly, matching the logic in configure.swift. The service is configured to automatically restart unless it’s explicitly stopped, making it more resilient in development or production environments.

    Build docker image and launch container by typing;

    docker composeup --build
    Screenshot

    Vapor server is ready, now lets call the endpoint for chacking that all is in place:

    Screenshot

    Configure Redis for avoiding DoS attacks

    A Denial-of-Service (DoS) attack occurs when a service is overwhelmed with excessive or malicious requests, exhausting its resources and preventing legitimate users from accessing it. Redis helps mitigate these attacks by serving as a fast, in-memory store that enables efficient rate limiting and request tracking; Vapor can use Redis to count requests per user or IP and reject or throttle those that exceed safe limits. By blocking abusive traffic early and cheaply, Redis prevents the application from being overloaded, keeping the service stable and responsive even under high or hostile load.

    First step is adding Redis library in Package.swift:

    // swift-tools-version:5.9
    import PackageDescription
    
    let package = Package(
       ...
        dependencies: [
           ...
            .package(url: "https://github.com/vapor/redis.git", from: "4.0.0")
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                   ...
                    .product(name: "Redis", package: "redis")
                ],
                ...
        ]
    )
    

    Next step is configure Redis in configure.swift:

    import Vapor
    import Redis
    
    public func configure(_ app: Application) throws {
        ...
        // Config Redis
        let redisHostname = Environment.get("REDIS_HOST") ?? "redis"
        let redisPort = Environment.get("REDIS_PORT").flatMap(Int.init) ?? 6379
    
        app.redis.configuration = try .init(
            hostname: redisHostname,
            port: redisPort
        )
    
        // Middleware de rate limit
        app.middleware.use(RateLimitMiddleware())
    
        // Rutas
        try routes(app)
    }
    

    This code configures a Vapor application to connect to a Redis instance by reading its host and port from environment variables, which is useful when running in Docker or cloud environments. It retrieves REDIS_HOST and REDIS_PORT, falling back to "redis" and port 6379 if they aren’t provided, ensuring sensible defaults when using a Redis container. It then applies these values to app.redis.configuration, enabling the application to communicate with Redis for features such as caching, rate limiting, or request tracking.

    For easy testing, we’ll define a simple DoS protection rule stating that the /hello endpoint cannot be called more than twice every 30 seconds. This rule is implemented in RateLimitMiddleware.swift.

    import Vapor
    import Redis
    
    struct RateLimitMiddleware: AsyncMiddleware {
    
        // Maximum 2 request every 30 secs
        private let maxRequests = 2
        private let windowSeconds = 30
    
        func respond(
            to request: Request,
            chainingTo next: AsyncResponder
        ) async throws -> Response {
    
            // Only apply to /hello service
            guard request.url.path == "/hello" else {
                return try await next.respond(to: request)
            }
    
            let ip = request.remoteAddress?.ipAddress ?? "unknown"
            let key = "rate:\(ip)"
    
            // INCR key
            let incrResponse = try await request.redis.send(
                command: "INCR",
                with: [RESPValue(from: key)]
            )
    
            let newCount = incrResponse.int ?? 0
    
            // When is first time, we set window expiration
            if newCount == 1 {
                _ = try await request.redis.send(
                    command: "EXPIRE",
                    with: [
                        RESPValue(from: key),
                        RESPValue(from: windowSeconds)
                    ]
                )
            }
    
            // Limit overpassed
            if newCount > maxRequests {
                throw Abort(
                    .tooManyRequests,
                    reason: "You exeded the limit of 2 request every 30 secs on /hello endpoint."
                )
            }
    
            return try await next.respond(to: request)
        }
    }
    

    It defines an asynchronous rate-limiting middleware for Vapor that restricts access to the /hello endpoint by tracking requests in Redis. It identifies the client by IP address, increments a Redis counter (INCR) associated with that IP, and, on the first request within the time window, sets an expiration (EXPIRE) so the counter resets after 30 seconds. If the number of requests exceeds 2 within 30 seconds, the middleware throws a 429 Too Many Requests error with a descriptive message; otherwise, it allows the request to continue through the normal processing chain. This mechanism helps prevent abuse or DoS-like behavior on that specific route.

    Last but not least, we need to update our Docker configuration. The first step is to add the Redis environment variables to the Dockerfile.

    ...
    # In order vapor listens inside Docker
    ENV PORT=8080
    ENV HOSTNAME=0.0.0.0
    ENV REDIS_HOST=redis
    ENV REDIS_PORT=6379
    
    CMD ["./Run"]
    

    And finally update docker-compose.yml for adding Redis service and connect vapor-app with Redis.

    version: "3.8"
    
    services:
      vapor-app:
        build: .
        ports:
          - "8080:8080"
        environment:
          PORT: "8080"
          HOSTNAME: "0.0.0.0"
          REDIS_HOST: "redis"
          REDIS_PORT: "6379"
        depends_on:
          - redis
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        container_name: redis-vapor
        ports:
          - "6379:6379"
        restart: unless-stopped

    Rebuild the image and launch the container:

    Screenshot

    Call /hello endpoint 3 times:

    Screenshot

    The rule that we have set is not very realistic, but is a clear example

    Conclusions

    Once you start implementing backend services, protecting your code against potential attacks becomes essential. The goal of this post was to show you how to build a simple defense mechanism against DoS attacks when working with Vapor, helping you keep your services stable, secure, and resilient under unexpected or malicious traffic.

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

    References

  • Customizing Vapor Server Configurations

    Customizing Vapor Server Configurations

    Customizing environment variables and secrets for a Dockerized Vapor server bridges mobile app development with backend security and DevOps best practices. iOS developers working with Swift-based Vapor servers need to securely manage API keys, database credentials, and other sensitive configurations, especially in containerized environments. This post explores how to set up, inject, and manage these secrets effectively within Docker, helping developers build more secure and scalable backend solutions while enhancing their understanding of environment-based configuration management.

    In this post, we will configure a simple Vapor server and demonstrate how to set up custom environment variables and provide secrets to the app.

    Hello Vapor Server

    The first step is to create a Vapor project by entering the following command:

    vapor new HelloServer

    You will be asked about Fluent and Leaf, but we will not be working with databases (Fluent) or HTML templates (Leaf).

    Screenshot

    Navigate to the created folder. In our case, we will use Xcode as the source code editor.

    Open routes.swift file:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            let name = Environment.get("CUSTOM_VARIABLE") ?? "World"
            return "Hello, \(name)!"
        }
    }

    The given Swift code defines a simple web route using the Vapor framework. It sets up a GET endpoint at «/hello», which returns a greeting message. The message includes a name retrieved from an environment variable called "CUSTOM_VARIABLE"; if this variable is not set, it defaults to "World". So, when a user accesses GET /hello, the server responds with "Hello, [name]!", where [name] is either the value of "CUSTOM_VARIABLE" or "World" if the variable is absent

    Custom Configuration variables

    During project creation one of the files generated was a Dockerfile, this file without any update will be used for building a Docker image:

    docker build -t hello-vapor .

    Finally we will run the container:

    docker run -e CUSTOM_VARIABLE="Custom variable Value" -p 8080:8080 hello-vapor

    The command docker run -e CUSTOM_VARIABLE="Custom variable Value" -p 8080:8080 hello-vapor runs a Docker container from the hello-vapor image. It sets an environment variable CUSTOM_VARIABLE with the value "Custom variable Value" inside the container using the -e flag. The -p 8080:8080 flag maps port 8080 of the container to port 8080 on the host machine, allowing external access to any service running on that port inside the container.

    The Vapor server is up and waiting for ‘hello’ GET requests. Open an new terminal session window and type following command.

    curl http://localhost:8080/hello

    When we get back to server terminal window:

    The endpoint implementation accesses the CUSTOM_VARIABLE environment variable and prints its contents.

    Secrets

    When we talk about transferring sensitive information such as keys, passwords, and API tokens, there is no perfect solution, as even the most secure methods come with potential vulnerabilities. Here’s an overview of common approaches and their security concerns:

    1. Storing secrets in a .env file: This approach keeps secrets separate from the code but still requires careful management of the .env file to prevent unauthorized access.

    2. Using Docker Secrets with Docker Compose: Docker Secrets allow you to store sensitive data in encrypted files, which can be mounted into containers. However, careful access control is necessary to prevent unauthorized retrieval.

    3. Implementing a sidecar container for secret management: A separate container can handle secret retrieval and securely pass them to the main application container. Access control rules can be applied to limit access to a specific set of users.

    4. Employing external secret management tools: Solutions like HashiCorp Vault or cloud-based key management services provide robust secret handling and enhanced security features, but they may introduce complexity in management.

    In this guide, we will store the secrets in a .env file. The first step is to create a text file containing the key-value pairs, naming it .env.

    echo "SECRET=Asg992fA83bs7d==" >> .env

    Keep the file in a safe place but never in a repository.

    Update endpoint code for also fetching SECRET:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            let name = Environment.get("CUSTOM_VARIABLE") ?? "World"
            let secret = Environment.get("SECRET") ?? "---"
            return "Hello, \(name)! secret is \(secret)"
        }
    }
    

    Build a the new docker image:

    docker build -t hello-vapor .

    For executing container, as input parameter we provide the .env file that conains the secret:

    docker run --env-file .env -p 8080:8080 hello-vapor

    Server iss ready again:

    Calling endpoint again:

     

    curl http://localhost:8080/hello

    Secreta is now fetched by the endpoint.

    Conclusions

    For entering environment variables there is an standarized way of doing but we can not say the same when it comes to talk about secrets. Depending on the degree of security that we want to achieve we will have to apply one method or other.

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

    References