Autor: admin

  • Attesting Secure Enclave generated keys

    Attesting Secure Enclave generated keys

    Why Attesting Keys Matter

    When your app creates a standard public/private key pair inside the Secure Enclave, the private key never leaves the hardware chip. However, if you send just the public key to a backend server, the server has a trust problem: How does it know that public key actually belongs to a secure hardware component on a genuine device, and not an attacker’s script pretending to be one?

    An attesting key solves this by providing a cryptographic chain of custody:

    1. The Attested Key: The regular key pair you generated for your app’s workflow (e.g., signing data).

    2. The Attestation Object: A cryptographic package generated by the Secure Enclave that bundles your attested public key together with device properties (like hardware identifiers and boot state).

    3. The Root of Trust (The Attesting Key/Certificate): The Secure Enclave signs this entire package using a built-in, unextractable hardware-level key (backed ultimately by Apple’s manufacturing root certificates).

    When your server receives this package, it can verify the signature back to Apple’s root authority. This gives the server mathematical proof that your app’s key is hardware-bound and untampered.

    Here is a quick summary of the Secure Enclave attestation process:

    • Challenge: The backend server sends a random, one-time challenge (nonce) to the app.

    • Signing: The Secure Enclave signs this challenge using the hardware-bound key, bundling the generated public key and device metadata.

    • Verification: The app sends this signed package to the backend, which cryptographically verifies it against Apple’s root certificates to prove the key genuinely originated from physical hardware.

    Challenge

    We will start on the receiver in our case we handle it in a new python script:

    TEAM_ID = "9999999999"       # Example: "ABCDE12345"
    BUNDLE_ID = "xxx.yyy.zzz" # Exact Bundle ID of your app in Xcode
    ENVIRONMENT = "development"             # "development" or "production"
    
    def run_attestation_server():
        print("=== STEP 1: Generate Challenge for the App ===")
        challenge_bytes = os.urandom(32)
        challenge_base64 = base64.b64encode(challenge_bytes).decode('utf-8')

    In this chunk you will have also to define some Security App project identifiers, for easily retrieve DEVELOPMENT_TEAM and PRODUCT_BUNDLE_IDENTIFIER just type following command on terminal:

    grep -rn -E "DEVELOPMENT_TEAM|PRODUCT_BUNDLE_IDENTIFIER"

    Next generates a cryptographically secure, 32-byte random challenge using the operating system’s secure random source and encodes it into a Base64 string so that it can be safely transmitted over a network to a client device for validation.

    Now execute python script for generating challenge:

    We will need to copy base64 challenge in iOS Security App for continuing with Signing process.

    Signing

    First step on adapting changes on Security app is adding App Test Capability on SegureApp target:

    Now move on to Security App for generating the attesting signature:

        @State private var attestationChallengeInput = ""
        @State private var attestedKeyId: String = ""
        @State private var attestationResultDisplay = "No attestation performed yet..."

    These three SwiftUI state variables manage the data flow and user interface state for the Security App Attest verification workflow, where attestationChallengeInput captures the Base64 challenge pasted from Python to compute its SHA-256 hash for Apple’s service, attestedKeyId stores the unique hardware key identifier returned by Apple to link the public key with the backend validation, and attestationResultDisplay controls the text label providing real-time feedback or displaying the resulting Base64 Attestation Object upon success.

    func performAppAttestation() {
                let service = DCAppAttestService.shared
                
                guard service.isSupported else {
                    attestationResultDisplay = "Error: App Attest not supported on this device/simulator."
                    return
                }
                
                guard let challengeData = Data(base64Encoded: attestationChallengeInput) else {
                    attestationResultDisplay = "Error: Invalid Base64 challenge string."
                    return
                }
                
                let clientDataHash = SHA256.hash(data: challengeData)
                
                service.generateKey { keyId, error in
                    if let error = error {
                        DispatchQueue.main.async {
                            attestationResultDisplay = "Error generating attest key: \(error.localizedDescription)"
                        }
                        return
                    }
                    
                    guard let keyId = keyId else { return }
                    self.attestedKeyId = keyId
                    
                    service.attestKey(keyId, clientDataHash: Data(clientDataHash)) { attestationObject, error in
                        DispatchQueue.main.async {
                            if let error = error {
                                attestationResultDisplay = "Attestation failed: \(error.localizedDescription)"
                                return
                            }
                            
                            guard let attestationObject = attestationObject else {
                                attestationResultDisplay = "Error: Empty attestation object received."
                                return
                            }
                            
                            let attestationBase64 = attestationObject.base64EncodedString()
                            self.attestationResultDisplay = attestationBase64
                            
                            print("\n--- ATTESTATION OBJECT FOR PYTHON ---")
                            print("Key ID: \(keyId)")
                            print("Attestation Object (Base64): \(attestationBase64)")
                            print("-------------------------------------\n")
                            
                            statusLog = "App Attest completed successfully 🛡️!"
                        }
                    }
                }
            }

    This function handles the core logic for executing Apple’s App Attest service on iOS, verifying device integrity and generating hardware-backed cryptographic credentials.

    Here is a step-by-step breakdown of what it does:

    • Environment & Input Validation: It first checks if App Attest is supported on the current device/simulator via DCAppAttestService.shared.isSupported. Then, it decodes the Base64 challenge string (attestationChallengeInput) provided by the user.

    • Client Data Hash Computation: It computes a SHA-256 cryptographic hash of the decoded challenge (clientDataHash), which prevents replay attacks by binding the challenge to the upcoming hardware attestation request.

    • Secure Enclave Key Generation: It calls service.generateKey to command the device’s Secure Enclave to generate a new public/private key pair, saving the resulting identifier (keyId) into attestedKeyId.

    • Hardware Attestation: It takes that keyId and the clientDataHash and invokes service.attestKey, asking Apple’s servers to cryptographically certify that the key was genuinely created in the device’s Secure Enclave.

    • Result Processing & Output: Upon success, it converts the received attestation object into a Base64 string, updates the UI display state, prints the formatted data directly to the Xcode console (for copying into your Python backend files), and logs a completion message.

    Finally add a new SwiftUI section on the app for pasting previous base64 generated challenge:

                    Section(header: Text("4. App Attest Verification")) {
                        TextField("Paste challenge from Python (Base64)", text: $attestationChallengeInput)
                            .textFieldStyle(.roundedBorder)
                            .autocapitalization(.none)
                            .disableAutocorrection(true)
                        
                        Button(action: performAppAttestation) {
                            HStack {
                                Image(systemName: "checkmark.shield.fill")
                                Text("Verify Key with Apple App Attest")
                            }
                        }
                        .disabled(!isKeyCreated || attestationChallengeInput.isEmpty)
                        
                        Text(attestationResultDisplay)
                            .font(.system(.caption2, design: .monospaced))
                            .foregroundColor(.purple)
                            .textSelection(.enabled)
                    }

    Important! Deploy the app in in real iPhone device and keep opened debuging XCode windows because there will be presented Key Id and Attestation Object

    Save the key ID and the attestation object to a file. A file is necessary because the attestation object is too large to copy and paste manually.

    Verification

    Once the key ID and attestation data are properly saved to their respective files, we can move on to the second and final part of the Python verification script.

        print("=== STEP 2: Reading Data from Files ===")
        print("Please save the Key ID in a file named 'key_id.b64'")
        print("and the Attestation Object (Base64) in a file named 'attestation.b64'")
        print("inside this same folder, and press ENTER to continue...")
        input()
        
        try:
            with open("key_id.b64", "r") as f:
                key_id_input = f.read().strip()
                
            with open("attestation.b64", "r") as f:
                attestation_base64_input = f.read().strip().replace("\n", "").replace(" ", "")
                
        except FileNotFoundError as e:
            print(f"❌ One of the required files was not found: {e}")
            return
    
        try:
            key_id_bytes = base64.b64decode(key_id_input)
            attestation_bytes = base64.b64decode(attestation_base64_input)
        except Exception as e:
            print(f"❌ Error decoding Base64 (check that there are no extra line breaks or spaces): {e}")
            return
    
        print("\n=== STEP 3: Validating Apple Attestation Object ===")
        
        try:
            attestation_object = loads(attestation_bytes)
        except Exception as e:
            print(f"❌ Error decoding CBOR: {e}")
            return
    
        fmt = attestation_object.get('fmt')
        if fmt != 'apple-appattest':
            print(f"❌ Invalid attestation format: {fmt}")
            return
        
        auth_data = attestation_object['authData']
        att_stmt = attestation_object['attStmt']
        x5c = att_stmt['x5c']
        
        if not x5c or len(x5c) < 1:
            print("❌ No certificates found in the x5c chain.")
            return
    
        cred_cert_bytes = x5c[0]
        cred_cert = x509.load_der_x509_certificate(cred_cert_bytes)
    
        aaguid = auth_data[37:53]
        expected_aaguid = b'appattestdev\x00\x00\x00\x00' if ENVIRONMENT == "development" else b'appattest\x00\x00\x00\x00\x00\x00\x00'
        
        if aaguid != expected_aaguid:
            print(f"⚠️️ Warning: AAGUID does not match the expected environment. Received: {aaguid}")
        else:
            print("✅ AAGUID verified successfully.")
    
        app_id = f"{TEAM_ID}.{BUNDLE_ID}"
        expected_rp_id_hash = hashlib.sha256(app_id.encode('utf-8')).digest()
        rp_id_hash_from_auth = auth_data[0:32]
        
        if rp_id_hash_from_auth != expected_rp_id_hash:
            print("❌ RP ID Hash does not match your Team ID and Bundle ID.")
            return
        print("✅ RP ID Hash verified successfully.")
    
        client_data_hash = hashlib.sha256(challenge_bytes).digest()
        nonce = hashlib.sha256(auth_data + client_data_hash).digest()
    
        nonce_extracted = None
        for extension in cred_cert.extensions:
            if extension.oid.dotted_string == "1.2.840.113635.100.8.2":
                ext_value = extension.value.value
                try:
                    idx = ext_value.find(b'\x04\x20')
                    if idx != -1:
                        nonce_extracted = ext_value[idx+2 : idx+2+32]
                except Exception:
                    pass
    
        if nonce_extracted == nonce:
            print("✅ Nonce verified successfully (The device signed the current challenge).")
        else:
            print("❌ Nonce does not match! The attestation might be fake or replayed.")
            return
    
        public_key = cred_cert.public_key()
        pub_key_bytes = public_key.public_bytes(
            encoding=serialization.Encoding.X962,
            format=serialization.PublicFormat.UncompressedPoint
        )
        pub_key_hash = hashlib.sha256(pub_key_bytes).digest()
    
        if pub_key_hash == key_id_bytes:
            print("✅ Key ID and Public Key linked successfully.")
        else:
            print("❌ Key ID does not match the public key in the certificate.")
            return
    
        print("\n🎉 ATTESTATION SUCCESSFULLY VALIDATED! The device is genuine and the key

    Here is a breakdown of what each section does:

    • Reading and Decoding Files (Step 2):

      • The script pauses and waits for you to save the generated Key ID into key_id.b64 and the Attestation Object into attestation.b64.

      • It reads these files, strips any stray whitespace or newlines, and decodes them from Base64 back into raw bytes (key_id_bytes and attestation_bytes).

    • CBOR Decoding and Format Check:

      • It decodes the binary attestation object using the cbor2 library and verifies that its format identifier (fmt) is strictly set to apple-appattest.

    • AAGUID and Environment Verification:

      • It extracts the Authenticator Attestation GUID (AAGUID) from the authData structure to ensure the hardware environment matches your configuration (appattestdev for development).

    • RP ID Hash Verification:

      • It hashes your combined TEAM_ID and BUNDLE_ID and compares it against the application identifier hash embedded inside the attestation data to confirm ownership.

    • Nonce / Challenge Verification:

      • It recomputes the expected nonce using the original challenge and extracts Apple’s custom cryptographic extension (1.2.840.113635.100.8.2) from the leaf certificate to guarantee the device signed a live request (preventing replay attacks).

    • Public Key & Key ID Linkage:

      • It extracts the public key from the hardware certificate, hashes it, and verifies that it precisely matches the Key ID provided by the app.

    • Final Result:

      • If all cryptographic checks pass successfully, it outputs a final success message confirming that the device is genuine and backed by the Secure Enclave.

    Get back to termina python execution script, and if files were propperly set, then just press enter:

    Conclusions

    In our previous post, we explained how to sign data using the Secure Enclave. In this article, we will show you how to validate that a key pair was genuinely generated inside physical Apple Secure Enclave hardware.

    The Secure Enclave is integrated into the system-on-chip (SoC) of the following Apple devices:

    • iPhone (iPhone 5s and later)

    • iPad (iPad Air, iPad Pro, iPad mini, and standard iPad models)

    • Mac (All Apple Silicon M-series Macs, Intel Macs with the Apple T2 security chip, and MacBook Pro models with the T1 chip)

    • Apple Watch (Series 1 and later)

    • Apple TV (Apple TV HD and later)

    • Apple Vision Pro

    • HomePod & HomePod mini

    References

  • Signing iOS App data using Secure Enclave

    Signing iOS App data using Secure Enclave

    The aim of this pos is demystify Apple’s hardware-backed security, bridging the gap between low-level cryptographic APIs and practical application development. It addresses a high industry demand for robust mobile security—especially in fintech and authentication—by demonstrating a complete end-to-end workflow from biometric authorization on an iPhone to backend cryptographic verification in Python.

    Secure enclave

    The Secure Enclave is a dedicated, hardware-based security subsystem built directly into Apple’s system-on-chip (SoC). It operates completely isolated from the main processor (the Application Processor) and the operating system (iOS) to provide an extra layer of security.

    Here are its key hardware characteristics:

    • Isolated Processing & Storage: It features its own boot ROM, hardware random number generator (RNG), and dedicated cryptographic engines, with its memory encrypted dynamically.

    • Hardware-Bound Keys: Private keys generated within the Secure Enclave are physically bound to the device’s silicon. They cannot be exported, extracted, or backed up to iCloud, making them immune to software-based extraction even if the main OS kernel is compromised.

    • Biometric Integration: It works closely with the Secure Enclave processor to securely process and authorize actions via Touch ID or Face ID without exposing biometric templates to the rest of the system.

    Between iOS (running on the Application Processor) and the Secure Enclave, there is no traditional external communication bus (such as an open I2C, SPI, or UART trace running across a circuit board) because both components reside physically inside the same System on a Chip (SoC).

    Instead, they communicate internally through advanced hardware mechanisms and strict isolation protocols. Here is how the communication takes place and how it is secured:

    1. The Communication Channel: Hardware Mailboxes

    The Application Processor (AP) and the Secure Enclave Processor (SEP) communicate via a dedicated hardware mailbox system.

    • How it works: This is an internal, hardware-level messaging interface integrated directly into the silicon. The AP writes a request or command (e.g., asking to evaluate a keychain item or process a cryptographic operation) into a specific memory-mapped mailbox register, triggering an interrupt (doorbell signal) to the Secure Enclave.
    • Request/Response Only: The AP can only drop messages into this mailbox and wait for a reply. It cannot execute code inside the Secure Enclave, peek into its execution registers, or manipulate its internal state.

    2. How the Communications Are Secured (If No Bus Exists)

    Since there is no open physical bus to tap or eavesdrop on externally, security relies on deep hardware-level enforcement:

    • Strict Memory Isolation (The Memory Protection Engine):
      Although the Secure Enclave shares access to the device’s main DRAM for running its OS (sepOS), it uses a dedicated, cryptographically protected region. Every time the Secure Enclave reads or writes to its memory, hardware logic called the Memory Protection Engine (MPE) encrypts and decrypts the data on-the-fly using AES in XEX mode alongside cryptographic authentication tags (CMAC). If the Application Processor attempts to read this memory space directly, it only sees randomized ciphertext.
    • Independent Execution and Microkernel:
      The Secure Enclave runs its own stripped-down, highly secure microkernel (sepOS, based on L4) completely separate from iOS. Even if the entire iOS kernel is compromised by malware or a jailbreak, the attacker still has zero administrative control over the Secure Enclave because the software boundary and CPU privilege rings are strictly enforced by the SoC hardware.
    • Crypto-Bound Operations (OS-Bound Keys):
      When data or keys pass back and forth via the mailbox, they are heavily encrypted. Furthermore, keys generated inside the Secure Enclave are bound to the hardware via a unique device ID (UID) and the cryptographic hash of the operating system image itself, ensuring that intercepted data packets are entirely useless if captured or replayed outside of that precise hardware context.

    Key pair generation from Secure Sample iOS App

    Our sample app what it does is just sign a message. But for doing that first thing that app does is request to Secure Enclave element to generate a public and private key pair:

            let deleteQuery: [String: Any] = [
                kSecClass as String: kSecClassKey,
                kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
                kSecAttrKeyType as String: kSecAttrKeyTypeEC
            ]
            SecItemDelete(deleteQuery as CFDictionary)

    Cleanup Existing Keys (SecItemDelete)

    • Before generating a new key, it checks if a key with the same application tag already exists in the iOS Keychain and deletes it to prevent conflicts.

            var error: Unmanaged<CFError>?
            guard let accessControl = SecAccessControlCreateWithFlags(
                nil,
                kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                [.privateKeyUsage, .biometryCurrentSet],
                &error
            ) else {
                statusLog = "Access control error: \(error.debugDescription)"
                return
            }
    

    Define Security and Biometric Policies (SecAccessControlCreateWithFlags)

    • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: Ensures the key can only be accessed after the user has unlocked the device at least once since boot, and it will not be included in iCloud backups.

    • [.privateKeyUsage, .biometryCurrentSet]: Enforces that any usage of the private key requires Face ID or Touch ID authentication. Furthermore, .biometryCurrentSet ensures that if a user adds a new fingerprint or face to the device later, this specific key is automatically invalidated, protecting against unauthorized additions.

            let attributes: [String: Any] = [
                kSecAttrKeyType as String: kSecAttrKeyTypeEC,
                kSecAttrKeySizeInBits as String: 256,
                kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
                kSecPrivateKeyAttrs as String: [
                    kSecAttrIsPermanent as String: true,
                    kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
                    kSecAttrAccessControl: accessControl
                ]
            ]

    Configure Key Attributes (attributes)

    • Type & Size: Specifies an Elliptic Curve (EC) key of 256 bits (kSecAttrKeyTypeEC), which is the standard curve used for modern secure communications (like P-256 / secp256r1).

    • Token ID (kSecAttrTokenIDSecureEnclave): This is the most critical line. It commands iOS to route the key generation request down to the hardware Secure Enclave, ensuring the private key never leaves the secure coprocessor’s isolated memory.

    • Persistence: Sets kSecAttrIsPermanent to true so the reference is safely stored in the system Keychain.

            var publicKeyRef, privateKeyRef: SecKey?
            let status = SecKeyGeneratePair(attributes as CFDictionary, &publicKeyRef, &privateKeyRef)
    
            guard status == errSecSuccess, let pubKey = publicKeyRef else {
                statusLog = "Error generating key: (Code: \(status))"
                return
            }

    Generate the Key Pair (SecKeyGeneratePair)

    • Triggers the Secure Enclave hardware to mathematically generate the private/public key pair. The private key remains locked inside the Secure Enclave forever; it cannot be read by iOS or any app.

            var exportError: Unmanaged<CFError>?
            guard let pubKeyData = SecKeyCopyExternalRepresentation(pubKey, &exportError) else {
                statusLog = "Error exporting key"
                return
            }
            
            let pubKeyBase64 = (pubKeyData as Data).base64EncodedString()
            publicKeyDisplay = "Key ready (See console for PEM)"
            
            print("\n--- COPY THIS PUBLIC KEY FOR PYTHON ---")
            print(pubKeyBase64)
            print("---------------------------------------\n")
    
            isKeyCreated = true
            statusLog = "Success: Key created in Secure Enclave 🔒"

    Export and Print the Public Key

    • Extracts the raw bytes of the public key (SecKeyCopyExternalRepresentation), converts them into a Base64-encoded string, and prints them to the Xcode console.

    • Why? Because the public key is safe to share. You would typically send this Base64 string to a backend server or a script (like Python) so the server can use it to verify signatures created by your iOS app.

    Secure iOS App message signature

    Second task that sample iOS app does is request message signature. Message is as simple and as critical as a money transfer authorization.

    @State private var messageToSign = "Authorize transfer of 500€"

    What signature process does is following:

            let query: [String: Any] = [
                kSecClass as String: kSecClassKey,
                kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
                kSecAttrKeyType as String: kSecAttrKeyTypeEC,
                kSecReturnRef as String: true
            ]
    
            var item: CFTypeRef?
            let status = SecItemCopyMatching(query as CFDictionary, &item)
    
            guard status == errSecSuccess, let rawItem = item else {
                signatureResult = "Could not retrieve the private key."
                return
            }
            let privateKey = rawItem as! SecKey

    Retrieve the Private Key (SecItemCopyMatching)

    • When you get that SecKey object in Swift, you do not hold the raw private key bytes. Instead, you hold a local cryptographic proxy object (a reference handle).

            guard let dataToSign = messageToSign.data(using: .utf8) else { return }
    
            var error: Unmanaged<CFError>?
            guard let signature = SecKeyCreateSignature(
                privateKey,
                .ecdsaSignatureMessageX962SHA256,
                dataToSign as CFData,
                &error
            ) as Data? else {
                signatureResult = "Signature cancelled or failed"
                return
            }
    
            let hexSig = signature.map { String(format: "%02hhx", $0) }.joined()
            signatureResult = hexSig
            
            print("\n--- COPY THIS HEXADECIMAL SIGNATURE FOR PYTHON ---")
            print(hexSig)
            print("------------------------------------------------\n")
            
            statusLog = "Signature successfully generated by hardware!"

    Trigger Biometric Prompt & Execution

    When you call SecKeyCreateSignature passing that reference:

    1. iOS sends a command down the hardware mailbox to the Secure Enclave saying: «Hey, use the private key associated with this handle to sign this specific message data.»

    2. The Secure Enclave pops up the Face ID / Touch ID prompt.

    3. Once authenticated, the Secure Enclave performs the mathematical signing operation entirely inside its own secure silicon, using the private key that lives safely in its isolated memory.

    4. The Secure Enclave spits out only the resulting signature back to iOS.

    Secure iOS App wrap up

    From user point of view what app does is request user for key generation  and generate the signature (Cryptographic result) for a fixed message:

    For facilitating validation in XCode logs windows are printed out public key in base64 and also an hexa string with the generated signature:

    --- COPY THIS PUBLIC KEY FOR PYTHON ---
    BFKD2YajWTN+yF9SOmAk6mebyJMuqbc/suP1uu0azpmp+55xYRhKCL3nmQJgYyau8z5GgtuSf3EPT0yEdwZg8WA=
    ---------------------------------------
    
    --- COPY THIS HEXADECIMAL SIGNATURE FOR PYTHON ---
    3045022035b35a8779457aa631858c1b48008d50b343e3b44ed4102e6730c70784c76bd5022100d63a02633ee2f899b4d00d3a6651df13f881968b58a5dbe2f4c46f7cb8e1ed36
    ------------------------------------------------

    Validator script

    For validating signature we have created a Python script, let’s going to analyze it:

    # 1. Paste here only the Base64 string (without PEM headers)
    pub_key_base64 = "BFKD2YajWTN+yF9SOmAk6mebyJMuqbc/suP1uu0azpmp+55xYRhKCL3nmQJgYyau8z5GgtuSf3EPT0yEdwZg8WA="
    # 2. Your signature in Hexadecimal exactly as returned by the app
    hex_signature = "3045022035b35a8779457aa631858c1b48008d50b343e3b44ed4102e6730c70784c76bd5022100d63a02633ee2f899b4d00d3a6651df13f881968b58a5dbe2f4c46f7cb8e1ed36"
    
    # 3. The exact original message you signed in the app
    message = "Authorize transfer of 5000€".encode('utf-8')

    Inputs Preparation

    • pub_key_base64: Takes the Base64-encoded public key string that your iOS app printed to the console during key generation.

    • hex_signature: Takes the hexadecimal signature string produced by the iOS app when you signed the message using Face ID/Touch ID.

    • message: Recreates the exact original message string (Authorize transfer of 5000€) and encodes it into bytes using UTF-8. (Note: This must match the exact text signed on iOS, down to the last character and the euro symbol).

    •  Look out! message has been altered transfer value has been set to 5000 instead of  500

    try:
        # A. Decode the public key from Base64 to raw bytes
        pub_key_bytes = base64.b64decode(pub_key_base64)
    
        # B. Reconstruct the public key using the P-256 (secp256r1) curve
        public_key = ec.EllipticCurvePublicKey.from_encoded_point(
            ec.SECP256R1(), 
            pub_key_bytes
        )
    
        # C. Convert the hexadecimal signature to DER binary
        der_signature = binascii.unhexlify(hex_signature)
    
        # D. Verify the cryptographic signature
        public_key.verify(
            der_signature,
            message,
            ec.ECDSA(hashes.SHA256())
        )

    Execution and Verification (try...except block)

    • Step A: Decode the Public Key

      • base64.b64decode(pub_key_base64) converts the Base64 text string back into its raw binary byte format.

    • Step B: Reconstruct the Public Key Object

      • ec.EllipticCurvePublicKey.from_encoded_point(...) rebuilds the public key object in Python using the P-256 curve (secp256r1), which is the exact same elliptic curve standard used by the iOS Secure Enclave.

    • Step C: Convert the Signature

      • binascii.unhexlify(hex_signature) transforms the hexadecimal signature string back into binary DER format so the cryptographic library can read it.

    • Step D: Mathematical Verification

      • public_key.verify(...) runs the cryptographic math. It checks whether the public key can successfully verify that the provided signature matches the message using ECDSA with SHA-256 (ec.ECDSA(hashes.SHA256())).

        print("\nVERIFICATION SUCCESSFUL! 🟢")
        print("The cryptogram was legitimately signed by the Secure Enclave of your iOS device.\n")
    
    except Exception as e:
        print("\nVERIFICATION FAILED! 🔴")
        print("Error detail:", e, "\n")

    Result Output

    • If successful (VERIFICATION SUCCESSFUL! 🟢): The math checks out. Python confirms that the signature could only have been created by the corresponding private key living securely inside the iOS device’s hardware.

    • If it fails (VERIFICATION FAILED! 🔴): An exception is thrown (e.g., if someone tampered with the message text, if the signature was altered, or if the public/private key pairs don’t match), indicating an invalid or fraudulent signature.

    On executing validation script we can check that message was altered, as it was:

    After restoring back the original message:

    # 3. The exact original message you signed in the app
    message = "Authorize transfer of 500€".encode('utf-8')

    Conclusions

    Across this example I have presented how easily is to guarante that information that is generated on device can be securely signed.

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

    References

  • Decorator pattern in Swift

    Decorator pattern in Swift

    This post aims to give you a clear, intuitive perspective on the Decorator pattern. As a software developer, you have likely implemented this pattern countless times without realizing it.

    Its main advantage lies in its flexibility: because a decorator acts as an independent add-on, you can seamlessly attach or remove capabilities without disrupting the core system.

    The Decorator pattern

    Here is a refined, clear, and professional rewrite of the explanation of the Decorator Pattern, tailored for Swift developers:

    The core idea of this pattern is to dynamically wrap or stack behavior around a central component. Crucially, wrapping an object with layers of new functionality must never alter or break its core identity or fundamental behavior.

    Everyday Analogies:

    • Clothing: A person wearing a t-shirt is still a person. Adding a sweater adds a layer of protection, and putting a coat over that adds even more, but at their core, they remain the same person.

    • In astronomy: the Decorator pattern is best visualized as a Rocky Planet Core (the Decoratee) wrapped by concentric cosmic layers, such as an Atmosphere or a Ring System (the Decorators). Just as adding an atmosphere provides radiation filtering or adding rings reflects light, each outer layer enhances the primary celestial body with new features without altering its fundamental identity at the center

    • Coffee: Plain coffee becomes a café au lait when you add milk, and a cappuccino when you sprinkle cocoa powder on top—yet at its core, it remains coffee.

    Implementation in Swift:

    To preserve this core behavior in code, the Decorator pattern relies on two fundamental Swift principles:

    • Conformance to the Same Protocol: Both the core object (decoratee) and the wrapper (decorator) must conform to the exact same protocol. This ensures they are fully interchangeable to any client code.

    • Dependency Injection: The decorator receives the decoratee instance (typically via initializer injection) and delegates work to it. Any client interacting with the wrapper is seamlessly interacting with the underlying object, augmented by the new behavior.

    When we turn into Swift:

    protocol Component {
        func execute() -> String
    }
    
    struct Decoratee: Component {
        func execute() -> String {
            return "Base execution"
        }
    }
    
    class Decorator: Component {
        private let component: Component
    
        init(component: Component) {
            self.component = component
        }
    
        func execute() -> String {
            return component.execute()
        }
    }

    The compositiona and usage will be following:

    let coreObject : Component = Decoratee()
    let decoratedObject : Component = Decorator(component: coreObject)
    let result = decoratedObject.execute()
    
    print(result)

    Roles

    Despite its simplicity, the Decorator pattern typically manifests in four distinct functional roles based on how it interacts with the inputs and outputs of the decoratee:

    • Monitor: The decorator observes execution without modifying any input or output data (e.g., Loggers, Profilers, or Analytics trackers).

    • Guard / Interceptor: The decorator intercepts and modifies, validates, or blocks the input before it reaches the decoratee (e.g., Cache lookups, Rate limiters, DDoS filters, or Failed login counters).

    • Fallback / Output Formatter: The decorator transforms or supplies the output after the decoratee executes, leaving inputs untouched (e.g.   Currency formatters, or Response mappers).

    • Transformer: The decorator actively alters both the input and output data (e.g., Encryption layers, where incoming data is encrypted and outgoing data is decrypted).

    Across all four roles, the underlying principle remains unchanged: the core identity and essential business logic of the decoratee are never altered—even during data transformation, where the underlying information remains the same despite its changed representation.

     

    Monitor

    In this role no transformation is performed, is the most inoquoos, where you do not notice any behaviour but at the end is there, we can add a decorator for profiling any heavy task, but just adding on debug mode but not on release. 

    import Foundation
    
    // Interface & Decoratee
    protocol DataService {
        func fetch(query: String) async throws -> String
    }
    
    struct RemoteService: DataService {
        func fetch(query: String) async throws -> String {
            try await Task.sleep(for: .milliseconds(600))
            return "Data for '\(query)'"
        }
    }
    
    // Performance Monitor Decorator
    struct PerformanceMonitor: DataService {
        let wrapped: DataService
        var threshold: Duration = .milliseconds(500)
    
        func fetch(query: String) async throws -> String {
            let clock = ContinuousClock()
            let elapsed = try await clock.measure { _ = try await wrapped.fetch(query: query) }
            
            let ms = elapsed.components.seconds * 1000 + Int64(elapsed.components.attoseconds / 1_000_000_000_000_000)
            print("⏱️ Execution took \(ms)ms \(elapsed > threshold ? "⚠️ [THRESHOLD EXCEEDED]" : "")")
            
            return try await wrapped.fetch(query: query)
        }
    }

    By using a Factory pattern, you can seamlessly configure a decorator—such as injecting a logger or profiler—whenever the debug flag is enabled.

    enum ServiceFactory {
        static func makeDataService(isDebug: Bool) -> DataService {
            let baseService = RemoteService()
    
            if isDebug {
                return PerformanceMonitor(wrapped: baseService)
            }
            return baseService
        }
    }
    
    let isDebugMode = true 
    let serviceA: DataService = ServiceFactory.makeDataService(isDebug: isDebugMode)

    Guard / Interceptor

    This decorator acts as a guard / interceptor by evaluating incoming inputs and triggering a specific action—such as returning cached data—whenever a condition is met. The core behavior remains intact: the caller receives the requested data, completely agnostic of whether it originated from the cache or the underlying service.

    Here is an example:

    import Foundation
    
    // 1. Component
    protocol DataService {
        func fetchData(key: String) -> String
    }
    
    // 2. Decoratee (Core network fetch)
    struct NetworkDataService: DataService {
        func fetchData(key: String) -> String {
            print("🌐 Fetching from network...")
            return "Data for \(key)"
        }
    }
    
    // 3. Decorator (Caching layer)
    final class CacheDecorator: DataService {
        private let decoratee: DataService
        private var cache: [String: String] = [:]
    
        init(decoratee: DataService) {
            self.decoratee = decoratee
        }
    
        func fetchData(key: String) -> String {
            if let cached = cache[key] {
                print("⚡️ Returning from cache")
                return cached
            }
            let data = decoratee.fetchData(key: key)
            cache[key] = data
            return data
        }
    }

    When executed, both calls to fetchData return identical results. From the caller’s perspective, the behavior is indistinguishable; under the hood, however, different paths were taken: the first call fetched fresh data from the remote source, while the second served it directly from the cache.

    let service: DataService = CacheDecorator(decoratee: NetworkDataService())
    
    print(service.fetchData(key: "user")) // 🌐 Fetching from network... -> Data for user
    print(service.fetchData(key: "user")) // ⚡️ Returning from cache   -> Data for user

    Fallback / Output formatter

    In this scenario, the decorator acts exclusively on the output. While the formatted responses look different, they represent the exact same underlying value—a concept demonstrated by the following Currency Decorator example. Although the decorator returns different strings depending on the selected currency (e.g., USD or EUR), the monetary value remains identical after conversion.

    import Foundation
    
    // 1. Component Protocol
    protocol PriceService {
        func getFormattedPrice(for productID: String) async -> String
    }
    
    // 2. Concrete Component (Base: USD Format -> $100.50)
    struct BaseUSDPriceService: PriceService {
        private let rawPrices: [String: Decimal] = ["PROD-1": 100.50]
    
        func getFormattedPrice(for productID: String) async -> String {
            let price = rawPrices[productID] ?? 0.00
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            formatter.currencySymbol = "$"
            formatter.currencyGroupingSeparator = ","
            formatter.currencyDecimalSeparator = "."
            
            return formatter.string(from: price as NSDecimalNumber) ?? "$\(price)"
        }
    }
    
    // 3. Decorator (Output Formatter: Converts USD -> EUR Format -> 100,50 €)
    struct EURCurrencyDecorator: PriceService {
        let wrapped: PriceService
        let usdToEurRate: Decimal = 0.92 // Exchange rate conversion
    
        func getFormattedPrice(for productID: String) async -> String {
            // 1. Get raw formatted output from wrapped USD service
            let usdString = await wrapped.getFormattedPrice(for: productID)
            
            // 2. Extract numeric value from USD string
            let cleanedString = usdString.replacingOccurrences(of: "$", with: "")
                                         .replacingOccurrences(of: ",", with: "")
            guard let usdAmount = Decimal(string: cleanedString) else { return "0,00 €" }
    
            // 3. Convert currency and reformat (Symbol on the RIGHT)
            let eurAmount = usdAmount * usdToEurRate
            
            let formatter = NumberFormatter()
            formatter.numberStyle = .decimal
            formatter.minimumFractionDigits = 2
            formatter.maximumFractionDigits = 2
            formatter.decimalSeparator = ","
            formatter.groupingSeparator = "."
    
            let formattedNumber = formatter.string(from: eurAmount as NSDecimalNumber) ?? "\(eurAmount)"
            
            // Action on Exit: Transformed into EUR format
            return "\(formattedNumber) €" 
        }
    }

    To support a new currency, simply create another decorator that handles the conversion from USD to that specific currency.

    // Base Service (USD)
        let usdService: PriceService = BaseUSDPriceService()
        let usdPrice = await usdService.getFormattedPrice(for: "PROD-1")
        print("USD Base Output: \(usdPrice)") 
        // Output: $100.50
    
        // Decorator (EUR)
        let eurService: PriceService = EURCurrencyDecorator(wrapped: usdService)
        let eurPrice = await eurService.getFormattedPrice(for: "PROD-1")
        print("EUR Decorator Output: \(eurPrice)") 
        // Output: 92,46 €

    Transformer

    In this scenario, the decorator transforms inputs on the way in and reverses the operation on outputs on the way out. This dual role is well illustrated by a cryptography layer: while the client always interacts with clear, unencrypted data, the decorator automatically ciphers it before storage and deciphers it upon retrieval.

    import Foundation
    import CryptoKit
    
    // 1. Interface & Raw Storage (Decoratee)
    protocol StorageService {
        func save(id: String, payload: Data)
        func load(id: String) -> Data?
    }
    
    class RawStorageService: StorageService {
        private var db = [String: Data]()
    
        func save(id: String, payload: Data) {
            db[id] = payload
            print("💾 [Storage] Saved \(payload.count) raw bytes for '\(id)'")
        }
    
        func load(id: String) -> Data? {
            print("📖 [Storage] Reading raw bytes for '\(id)'")
            return db[id]
        }
    }
    
    // 2. Transformer Decorator (Encrypts on save, Decrypts on load)
    struct CryptoDecorator: StorageService {
        let wrapped: StorageService
        let key: SymmetricKey
    
        func save(id: String, payload: Data) {
            // ENTRANCE: Encrypt Plaintext -> Ciphertext before storing
            guard let sealedBox = try? AES.GCM.seal(payload, using: key),
                  let ciphertext = sealedBox.combined else { return }
            
            print("🔐 [Crypto] Encrypted \(payload.count) bytes -> \(ciphertext.count) bytes")
            wrapped.save(id: id, payload: ciphertext)
        }
    
        func load(id: String) -> Data? {
            // EXIT: Fetch Ciphertext -> Decrypt back to Plaintext
            guard let ciphertext = wrapped.load(id: id),
                  let box = try? AES.GCM.SealedBox(combined: ciphertext),
                  let plaintext = try? AES.GCM.open(box, using: key) else { return nil }
            
            print("🔓 [Crypto] Decrypted \(ciphertext.count) bytes back to original plaintext")
            return plaintext
        }
    }

    Its usage is as follows:

    let key = SymmetricKey(size: .bits256)
    let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
    
    let originalData = "Secret User Token".data(using: .utf8)!
    
    // 1. Save (Encrypts on Entry)
    storage.save(id: "user_session", payload: originalData)
    
    // 2. Load (Decrypts on Exit)
    if let loadedData = storage.load(id: "user_session"),
       let decryptedString = String(data: loadedData, encoding: .utf8) {
        print("Received: '\(decryptedString)'")
    }

    Compositon of decocators

    Last but not least, composing multiple decorators is one of the core strengths of the Decorator pattern. Because every decorator implements the exact same interface as the component it wraps, you can seamlessly chain them together—nesting behavior like Russian dolls.


    Here is a complete usage example combining two separate decorators around a single storage component:

    let key = SymmetricKey(size: .bits256)
    let rawStorage = RawStorage()
    
    // Composition Chain: Client -> MonitorDecorator -> CryptoDecorator -> RawStorage
    let pipeline: StorageService = MonitorDecorator(
        wrapped: CryptoDecorator(
            wrapped: rawStorage,
            key: key
        )
    )
    
    let payload = "Secret Data".data(using: .utf8)!
    
    // Execution sequence:
    // 1. Monitor starts timer -> Calls Crypto
    // 2. Crypto encrypts payload -> Calls RawStorage
    // 3. RawStorage saves to memory
    // 4. Returns back through layers -> Monitor logs total duration
    pipeline.save(id: "config", payload: payload)
    let loaded = pipeline.load(id: "config")

    Its usage is as follows:

    let key = SymmetricKey(size: .bits256)
    let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
    
    let originalData = "Secret User Token".data(using: .utf8)!
    
    // 1. Save (Encrypts on Entry)
    storage.save(id: "user_session", payload: originalData)
    
    // 2. Load (Decrypts on Exit)
    if let loadedData = storage.load(id: "user_session"),
       let decryptedString = String(data: loadedData, encoding: .utf8) {
        print("Received: '\(decryptedString)'")
    }

    Conclusions

    Although the Decorator pattern can adopt different functional roles, confusing them for separate patterns is easy. At its core, the essence remains unchanged: adding or removing features dynamically alters capabilities, but never alters the fundamental nature of the underlying behavior.

    References

    There are two primary historical references linked to the formal documentation of the Decorator pattern:

    • 1994 (GoF Book): Design Patterns: Elements of Reusable Object-Oriented Software — Addison-Wesley published the definitive text defining the pattern.

    • 1991 (ET++ Framework / PhD Thesis): Erich Gamma’s doctoral dissertation at the University of Zurich, where early architectural precursors were documented prior to the GoF book.

  • 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