Categoría: Architecture

  • 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

  • Rules to write Clean Code in Swift

    Rules to write Clean Code in Swift

    In this post we will focusfocus on showing how principles like KISS, DRY, and SOLID directly improve Swift code in everyday situations—such as structuring a ViewModel, reducing boilerplate in SwiftUI views, or designing protocols for networking.

    In this post we will revieew some roles principles by examples in Swift. This is could be considerered a continuation of a past post called ‘S.O.L.I.D. principles in Swift’

    Clean Code

    Clean code is code that is simple to read, easy to understand, and straightforward to maintain. It avoids unnecessary complexity by following principles like clarity, consistency, and separation of concerns, ensuring that each part of the code has a clear purpose. Clean code favors meaningful names, small and focused functions, and eliminates duplication, making it easier for other developers (and your future self) to work with. The goal is not just to make the computer run the program, but to make the code itself communicate its intent clearly to humans.

    Clean Code is the overarching goal of writing software that is easy to read, understand, and maintain, while the SOLID principles are a concrete set of guidelines that help achieve this goal by structuring classes and dependencies in a clear, flexible way. In essence, Clean Code defines what good code should feel like, and SOLID offers how to design object-oriented code that stays true to that vision, ensuring maintainability, scalability, and clarity.

    SOC – Separation Of Concepts

    Separation of Concerns (SoC) is a clean code principle that says each part of your code should focus on a single, well-defined responsibility. By splitting concerns, you reduce complexity, improve readability, and make code easier to maintain and test. In iOS/Swift, this often means separating data models, networking logic, business logic (ViewModel), and UI (View) instead of mixing them in one place.

    import SwiftUI
    import Foundation
    
    // MARK: - Model (data only)
    struct Post: Decodable, Identifiable {
        let id: Int
        let title: String
    }
    
    // MARK: - Networking (concern: fetching data)
    final class PostService {
        func fetchPosts() async throws -> [Post] {
            let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
            let (data, _) = try await URLSession.shared.data(from: url)
            return try JSONDecoder().decode([Post].self, from: data)
        }
    }
    
    // MARK: - ViewModel (concern: state & business logic)
    @MainActor
    final class PostListViewModel: ObservableObject {
        @Published var posts: [Post] = []
        @Published var errorMessage: String?
    
        private let service = PostService()
    
        func loadPosts() async {
            do {
                posts = try await service.fetchPosts()
            } catch {
                errorMessage = "Failed to load posts"
            }
        }
    }
    
    // MARK: - View (concern: UI)
    struct PostListView: View {
        @StateObject private var vm = PostListViewModel()
    
        var body: some View {
            NavigationView {
                List(vm.posts) { post in
                    Text(post.title)
                }
                .navigationTitle("Posts")
                .task { await vm.loadPosts() }
            }
        }
    }
    

    Where’s the SoC?

    • Model (Post) → defines the data only.

    • Service (PostService) → knows how to fetch posts from the network.

    • ViewModel (PostListViewModel) → manages state and error handling.

    • View (PostListView) → displays UI and reacts to state changes.

    Each layer does one thing well, instead of mixing networking, logic, and UI together.

    DRY – Do not repeat yourself

    DRY (Don’t Repeat Yourself) is a clean code principle that says you should avoid duplicating logic across your codebase. Instead, extract common behavior into reusable functions, extensions, or abstractions. This reduces errors, makes updates easier, and improves maintainability.

    ❌ Without DRY (duplicated logic)

    struct User {
        let name: String
        let email: String
    }
    
    struct Product {
        let title: String
        let price: Double
    }
    
    func printUser(_ user: User) {
        print("----")
        print("Name: \(user.name)")
        print("Email: \(user.email)")
        print("----")
    }
    
    func printProduct(_ product: Product) {
        print("----")
        print("Title: \(product.title)")
        print("Price: \(product.price)")
        print("----")
    }
    

    The logic for printing the separators (----) and formatting output is repeated in both functions.

    ✅ With DRY (reusable abstraction)

    protocol Printable {
        var descriptionLines: [String] { get }
    }
    
    struct User: Printable {
        let name: String
        let email: String
        
        var descriptionLines: [String] {
            [
                "Name: \(name)",
                "Email: \(email)"
            ]
        }
    }
    
    struct Product: Printable {
        let title: String
        let price: Double
        
        var descriptionLines: [String] {
            [
                "Title: \(title)",
                "Price: \(price)"
            ]
        }
    }
    
    // Generic reusable function
    func printEntity(_ entity: Printable) {
        print("----")
        entity.descriptionLines.forEach { print($0) }
        print("----")
    }
    
    // Usage
    let user = User(name: "Alice", email: "alice@email.com")
    let product = Product(title: "iPhone", price: 999.99)
    
    printEntity(user)
    printEntity(product)
    

    Now, the separator and printing logic exist in one place (printEntity). If we want to change the format, we update it once, not in every function.

    KISS-Keep it simple stupid

    KISS (Keep It Simple, Stupid) is a clean code principle that emphasizes writing code in the simplest way possible, avoiding unnecessary complexity or over-engineering. The goal is clarity: solve the problem directly instead of anticipating extra cases you don’t need yet.

    ❌ Not KISS (too complex for a simple counter)

    import SwiftUI
    
    struct ComplexCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("Increment") { updateCount(by: 1) }
                    Button("Decrement") { updateCount(by: -1) }
                }
            }
        }
        
        // Over-abstracted logic for a simple task
        private func updateCount(by value: Int) {
            let newValue = count + value
            if newValue >= 0 {
                count = newValue
            } else {
                count = 0
            }
        }
    }
    

    Too much abstraction (updateCount) and rules for a trivial counter.


    ✅ KISS (straightforward and clear)

    import SwiftUI
    
    struct SimpleCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("−") { if count > 0 { count -= 1 } }
                    Button("+") { count += 1 }
                }
            }
        }
    }
    

    The code is direct, easy to read, and does only what’s needed.

    DYC-Document your code

    DYC (Document Your Code) is a clean code principle that emphasizes writing code that is self-explanatory whenever possible, but also adding clear, concise documentation when something isn’t obvious. In Swift, this is often done with /// doc comments, which Xcode uses to show inline documentation when hovering over symbols. The idea is to make the code not just work, but also easy to understand and use for other developers (or your future self).

    ✅ Example in Swift using DYC

    import Foundation
    
    /// Represents a user in the system.
    struct User {
        /// Unique identifier for the user.
        let id: Int
        
        /// The user's display name.
        let name: String
        
        /// The user's email address.
        let email: String
    }
    
    /// Protocol defining operations to fetch users.
    protocol UserRepository {
        /// Fetches all available users from a data source.
        /// - Returns: An array of `User` objects.
        /// - Throws: An error if fetching fails.
        func fetchUsers() async throws -> [User]
    }
    
    /// Remote implementation of `UserRepository` that calls an API.
    final class RemoteUserRepository: UserRepository {
        private let baseURL: URL
        
        /// Creates a new repository with the given API base URL.
        /// - Parameter baseURL: The base URL of the remote API.
        init(baseURL: URL) {
            self.baseURL = baseURL
        }
        
        /// Calls the `/users` endpoint and decodes the response into a list of `User`.
        /// - Throws: `URLError` if the network call fails or the response is invalid.
        /// - Returns: An array of users retrieved from the server.
        func fetchUsers() async throws -> [User] {
            let url = baseURL.appendingPathComponent("users")
            let (data, response) = try await URLSession.shared.data(from: url)
            
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                throw URLError(.badServerResponse)
            }
            
            return try JSONDecoder().decode([User].self, from: data)
        }
    }
    

    Why this follows DYC

    • Each type and method has a short description of what it does.

    • Methods document parameters, return values, and possible errors.

    • Xcode can show these comments in Quick Help (Option + Click) for better developer experience.

    YAGNI-You’re not going to need it

    YAGNI (You Aren’t Gonna Need It) is a clean code principle that reminds developers not to add functionality until it’s actually needed. Writing “just in case” code leads to complexity, unused features, and harder maintenance. Instead, implement only what the current requirements demand, and extend later when there’s a real need.


    ❌ Violating YAGNI (adding unnecessary features)

    import SwiftUI
    
    struct OverEngineeredCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("Increment") { updateCount(by: 1) }
                    Button("Decrement") { updateCount(by: -1) }
                    Button("Reset") { resetCount() }          // Not needed (yet)
                    Button("Multiply ×2") { multiplyCount() } // Not needed (yet)
                }
            }
        }
        
        private func updateCount(by value: Int) {
            count += value
        }
        
        private func resetCount() { count = 0 }
        private func multiplyCount() { count *= 2 }
    }
    

    Only increment and decrement are required, but reset and multiply were added “just in case.”


    ✅ Following YAGNI (keep it simple until needed)

    import SwiftUI
    
    struct SimpleCounterView: View {
        @State private var count = 0
        
        var body: some View {
            VStack {
                Text("Count: \(count)")
                    .font(.largeTitle)
                
                HStack {
                    Button("−") { count -= 1 }
                    Button("+") { count += 1 }
                }
            }
        }
    }
    

    The code contains only the features needed today. If requirements change, new functionality can be added later.

    LoD-Law of Demeter

    The Law of Demeter (LoD), also called the principle of least knowledge, says:
    A unit of code should only talk to its immediate collaborators (“friends”), not to the internals of those collaborators (“friends of friends”)’.

    In practice, this means avoiding long chains like order.customer.address.city, because it creates tight coupling and makes the code fragile if internal structures change. Instead, each object should expose only what is necessary.

    ❌ Violating the Law of Demeter

    struct Address {
        let city: String
    }
    
    struct Customer {
        let address: Address
    }
    
    struct Order {
        let customer: Customer
    }
    
    func printOrderCity(order: Order) {
        // ❌ Too many “hops” (order → customer → address → city)
        print(order.customer.address.city)
    }
    

    If the internal structure of Customer or Address changes, this function breaks.


    ✅ Following the Law of Demeter

    struct Address {
        let city: String
    }
    
    struct Customer {
        private let address: Address
        
        init(address: Address) {
            self.address = address
        }
        
        /// Expose only what’s needed
        func getCity() -> String {
            return address.city
        }
    }
    
    struct Order {
        private let customer: Customer
        
        init(customer: Customer) {
            self.customer = customer
        }
        
        /// Expose only what’s needed
        func getCustomerCity() -> String {
            return customer.getCity()
        }
    }
    
    func printOrderCity(order: Order) {
        // ✅ Only talks to Order, not Order’s internals
        print(order.getCustomerCity())
    }
    

    Now printOrderCity only knows about Order. The details of Customer and Address are hidden, so changes in those types won’t ripple outward.

    Lesson:

    • ❌ Don’t chain through multiple objects (a.b.c.d).

    • ✅ Provide methods that expose the needed behavior directly.

    Conclusions

    We have demonstrated how the Clean principles align with Swift, so there is no excuse for not applying them. 

  • iOS Dev between Combine and Delegate/Closure

    iOS Dev between Combine and Delegate/Closure

    When to use Combine versus the delegate and closure approaches is a great point to clarify because many developers struggle to choose the right tool for different scenarios, especially as Apple continues to expand its reactive frameworks while traditional patterns remain widely used. This post pretends to highlight the trade-offs—such as Combine’s power in managing complex, asynchronous data streams and chaining operators, versus the simplicity and low-overhead of closures for one-off callbacks or delegates for structured, reusable communication.

    The dilemma

    Combine, delegates, and closures all solve the same core problem: communicating results or events between components asynchronously.

    Here’s what they share in common:

    • Asynchronous communication → all three let one part of your app notify another when “something happened” without blocking.

    • Decoupling → the caller doesn’t need to know how the callee works, only what result or event it sends back.

    • Custom API design → you can choose the style (reactive stream, callback, or protocol) that best fits your use case, but under the hood they’re all just passing messages across boundaries.

    • Memory management concerns → all require care with retain cycles ([weak self] in closures, weak delegates, cancellables in Combine).

    • Interoperability → you can wrap one into another (e.g., turn a delegate or closure into a Publisher, or call a closure inside a delegate method).

    In short: they’re different abstractions over the same idea of event handling and result delivery, just with different levels of structure and power.

    The rule of thumb

    Use Combine when…

    • You’re dealing with streams of values over time (state updates, network retries, text search with debounce, multi-source merges).

    • You need operator chains (map/filter/flatMap/throttle/retry/catch) and cancellable pipelines you can pause or tear down with a single AnyCancellable.

    • You’re deep in SwiftUI (@Published, ObservableObject, view models) or want fan-out to multiple subscribers.

    • Testing with schedulers and time (virtual clocks) matters.

    Use delegates/closures when…

    • It’s a one-shot or simple callback (“user picked photo”, “request finished”, “button tapped”).

    • You want minimal overhead & maximal clarity in local APIs; fewer allocations and less indirection.

    • The interaction is structured and two-way (classic UIKit delegate lifecycles) or you’re writing a library that shouldn’t force a reactive dependency.

    Examples

    Example (type-ahead search with debouncing & cancellation):

    import Combine
    import UIKit
    
    final class SearchViewModel {
        @Published var query: String = ""
        @Published private(set) var results: [Repo] = []
    
        private var bag = Set<AnyCancellable>()
        private let api = GitHubAPI()
    
        init() {
            $query
                .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
                .removeDuplicates()
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .filter { !$0.isEmpty }
                .flatMap { q in
                    self.api.searchRepos(query: q)              // -> AnyPublisher<[Repo], Error>
                        .replaceError(with: [])                 // surface errors as empty for UX
                }
                .receive(on: RunLoop.main)
                .assign(to: &$results)
        }
    }
    
    struct Repo: Decodable { let name: String }
    
    final class GitHubAPI {
        func searchRepos(query: String) -> AnyPublisher<[Repo], Error> {
            let url = URL(string: "https://api.github.com/search/repositories?q=\(query)")!
            return URLSession.shared.dataTaskPublisher(for: url)
                .map(\.data)
                .decode(type: SearchResponse.self, decoder: JSONDecoder())
                .map(\.items)
                .eraseToAnyPublisher()
        }
    }
    
    struct SearchResponse: Decodable { let items: [Repo] }
    

    Example (delegate for a child picker reporting a single selection):

    protocol ColorPickerViewControllerDelegate: AnyObject {
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor)
    }
    
    final class ColorPickerViewController: UIViewController {
        weak var delegate: ColorPickerViewControllerDelegate?
    
        // call once when a color is chosen
        private func didChoose(_ color: UIColor) {
            delegate?.colorPicker(self, didSelect: color)
            dismiss(animated: true)
        }
    }
    
    // Parent sets itself as delegate
    final class SettingsViewController: UIViewController, ColorPickerViewControllerDelegate {
        func presentPicker() {
            let picker = ColorPickerViewController()
            picker.delegate = self
            present(picker, animated: true)
        }
    
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor) {
            view.backgroundColor = color
        }
    }
    

    Closure variant (simple one-shot completion):

    final class ImageLoader {
        func load(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error { completion(.failure(e)); return }
                guard let data = data, let image = UIImage(data: data) else {
                    completion(.failure(NSError(domain: "decode", code: -1)))
                    return
                }
                completion(.success(image))
            }.resume()
        }
    }
    

    Quick decision checklist

    • Do I expect multiple values over time that I want to transform/combine? → Combine

    • Is it a single, directed callback (child → parent or one network response)? → Delegate/Closure

    • Do I need easy cancellation, debouncing, retries, or operator chains? → Combine

    • Is simplicity, minimal dependencies, or pre–iOS 13 support important? → Delegate/Closure

    💡Tip: don’t force reactive patterns everywhere—use Combine where its composability shines, and keep the rest lightweight.

    Bridge between them

    Combine and delegates/closures can absolutely be mixed in the same project, and in fact that’s often the most pragmatic approach. They aren’t mutually exclusive, they just solve different layers of the problem:

    • Frameworks & SDKs: Many Apple and third-party APIs still expose delegates or completion handlers (e.g. CLLocationManager, URLSession).

    • Your app architecture: You might want to stay “Combine-first” in your view models, but still need to talk to APIs that rely on delegate or closure callbacks.

    Example: turning a delegate into a Combine publisher:
    import Combine
    import CoreLocation
    
    final class LocationManagerDelegateProxy: NSObject, CLLocationManagerDelegate {
        let subject = PassthroughSubject<CLLocation, Never>()
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            if let loc = locations.last {
                subject.send(loc)
            }
        }
    }
    
    // Usage
    let locationManager = CLLocationManager()
    let proxy = LocationManagerDelegateProxy()
    locationManager.delegate = proxy
    
    let cancellable = proxy.subject
        .sink { location in
            print("Got location:", location)
        }
    

    Example: wrapping a closure in a publisher

    func loadImagePublisher(from url: URL) -> AnyPublisher<UIImage, Error> {
        Future { promise in
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error {
                    promise(.failure(e))
                } else if let data, let image = UIImage(data: data) {
                    promise(.success(image))
                } else {
                    promise(.failure(NSError(domain: "decode", code: -1)))
                }
            }.resume()
        }
        .eraseToAnyPublisher()
    }
    

    Conclusions

    I hope this helped clarify when it makes sense to use Combine and when a delegate or closure is the better fit. Most importantly, remember that these approaches are not mutually exclusive—you don’t need to force reactive patterns into every situation. Instead, use Combine where its strengths truly shine, and rely on delegates or closures when simplicity is all you need.

    References

    • Combine

      Apple Developer Documentation

  • Dependency Injection implementations in Swift

    Dependency Injection implementations in Swift

    How to use dependency injection both manually and with a library like Swinject is valuable because it helps developers understand the core principles behind DI, such as decoupling and testability, before introducing them to more scalable, flexible solutions. By comparing both approaches, you empower readers to make informed architectural decisions based on project complexity and team needs. It appeals to a broad audience—from beginners learning the basics to experienced developers seeking to streamline their code using frameworks—and highlights the real-world tradeoffs between control and convenience, making it a practical and educational resource.

    DIP-Dependency injection principle

    Dependency injection is a software design principle in which an object or function receives the resources or dependencies it needs from external sources rather than creating them itself, promoting loose coupling and greater flexibility in code. By separating the responsibility of constructing dependencies from their usage, dependency injection makes programs easier to test, maintain, and modify, since dependencies can be swapped or mocked without changing the dependent code. This approach is closely related to the inversion of control principle, as it shifts the creation and management of dependencies to an external entity, often called an injector or container, allowing for more modular and configurable systems.

    For our example the interface will be ‘UserService’:

    protocol UserService {
        func fetchUsers() -> [User]
    }
    
    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 1, name: "Alice"),
                User(id: 2, name: "Bob")
            ]
        }
    }
    

    This is how View model uses the UserService interface:

    class UserListViewModel: ObservableObject {
        @Published var users: [User] = []
    
        private let userService: UserService
    
        init(userService: UserService) {
            self.userService = userService
            loadUsers()
        }
    
        func loadUsers() {
            self.users = userService.fetchUsers()
        }
    }

    The view that presents user list:

    struct UserListView: View {
        @ObservedObject var viewModel: UserListViewModel
    
        var body: some View {
            List(viewModel.users) { user in
                Text(user.name)
            }
        }
    }

    …but where is dependency injection implemented? You’re probably thinking that right now. Hold on a sec…

    @main
    struct ManualDIApp: App {
        var body: some Scene {
            WindowGroup {
                let userService = DefaultUserService()
                let viewModel = UserListViewModel(userService: userService)
                UserListView(viewModel: viewModel)
            }
        }
    }
    

    At that point in the code, an instance of DefaultUserService—which implements the UserService protocol—is being injected into the viewModel.

    Dependency injection by using Swinject

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    First thing to do is add ‘https://github.com/Swinject/Swinject’ as SPM package:

    Look out! Add Swinject to SwinjectDI target, but Swinject-Dynamic to none. I faced compilations issues due to that.

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    We have to declare a new component that will be responsible for resolving dependencies:

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModel = DIContainer.shared.container.resolve(UserListViewModel.self)!
                UserListView(viewModel: viewModel)
            }
        }
    }

    To implement dependency injection, we simply call the resolver to fetch the appropriate instance to be injected into UserListView. Notice that UserListViewModel also depends on UserService, but this dependency is also resolved by the DIResolver. In conclusion, we can observe that the lines of code required to construct the dependency stack have been reduced to a single line.

    Handling different Protocol implementations

    What we explained in the previous section covers most cases where dependency injection needs to be implemented. However, what happens when we have different protocol implementations? For example, consider a scenario where the same view is used in different application flows, but the data sources differ—one fetches data from a database, while the other uses a REST API.

    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 1, name: "Alice")]
        }
    }
    
    class DefaultUserServiceV2: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 2, name: "Charlie")]
        }
    }
    

    We now have two classes that implement the UserService protocol. The following changes are required to build the dependency injection stack:

                // Screen 1
                let service1 = DefaultUserService()
                let viewModel1 = UserListViewModel(userService: service1)
    
                // Screen 2
                let service2 = DefaultUserServiceV2()
                let viewModel2 = UserListViewModel(userService: service2)

    View model is the same what it differs is the injected userService.

    Dependency injection by using Swinject

    The dependency injection stack usually consists of the same set of dependencies. This consistency is why third-party libraries like Swinject are beneficial—they take advantage of this common pattern.

    However, occasionally, you may encounter a rare case in your app where the dependency stack for a particular screen needs to be set up differently—for instance, when the data source differs.

    Here’s how the DIContainer resolves dependencies:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { r in
                let service = r.resolve(UserService.self, name: "v1")!
                return UserListViewModel(userService: service)
            }
    
            container.register(UserListViewModel.self, name: "v2") { r in
                let service = r.resolve(UserService.self, name: "v2")!
                return UserListViewModel(userService: service)
            }
        }
    }

    The solution was implemented by registering the instance type with a tag name. When implementing dependency injection, we need to provide an additional name tag parameter.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModelV1 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v1")!
                UserListView(viewModel: viewModelV1)
                
    //            let viewModelV2 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v2")!
    //            UserListView(viewModel: viewModelV2)
            }
        }
    }

    In the previous code chunk, "v1" is hardcoded, but it should be dynamic. Ideally, it should instantiate either viewModelV1 or viewModelV2 depending on the situation.

    Unit tests

    Dependency injection in unit testing typically involves injecting a mock that implements a protocol, allowing for deterministic and controlled responses.

    import Foundation
    @testable import ManualDI
    
    class DefaultUserServiceMock: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 99, name: "Mocked User")
            ]
        }
    }

    Unit tests will look something like this:

    import Testing
    @testable import ManualDI
    
    struct ManualDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let mock = DefaultUserServiceMock()
            let viewModel1 = UserListViewModel(userService: mock)
            
            #expect(viewModel1.users.count == 0)
        }
    
    }

    Dependency injection by using Swinject

    Unit test will have to be implemented in following way:

    import Testing
    import Swinject
    @testable import SwinjectDI
    
    struct SwinjectDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let testContainer = Container()
            
            testContainer.register(UserService.self) { _ in DefaultUserServiceMock() }
    
             testContainer.register(UserListViewModel.self) { r in
                 UserListViewModel(userService: r.resolve(UserService.self)!)
             }
    
             let viewModel = testContainer.resolve(UserListViewModel.self)!
            #expect(viewModel.users.first?.name == "Mocked User")
        }
    }

    Never use the app-wide DIContainer.shared in tests — always use a local test container so you can inject mocks safely and independently.

    @Injected properly wrapper

    One more thing… By using following property wrapper:

    import Swinject
    
    @propertyWrapper
    struct Injected<T> {
        private var service: T
    
        init(name: String? = nil) {
            if let name = name {
                self.service = DIContainer.shared.container.resolve(T.self, name: name)!
            } else {
                self.service = DIContainer.shared.container.resolve(T.self)!
            }
        }
    
        var wrappedValue: T {
            service
        }
    }

    DIContiner keeps more simplified:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { _ in UserListViewModel() }
            container.register(UserListViewModel.self, name: "v2") { _ in UserListViewModel() }
        }
    }

    And also viewmodel:

     class UserListViewModel: ObservableObject {
         @Published var users: [User] = []
    
         @Injected(name: "v1") private var userService: UserService
    
         func loadUsers() {
             self.users = userService.fetchUsers()
         }
     }

    Conclusions

    I did not try to convince you how useful Dependency Injection is; you can easily find information about it on the internet. Instead, I aim to show how, with a third-party library like Swinject, the process of setting up the Dependency Injection stack can be simplified.

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

    References