Swift Macros: Ejemplos prácticos de metaprogramación

Guía completa sobre Swift Macros: creación de macros freestanding y attached, manipulación del AST con swift-syntax y ejemplos prácticos para eliminar código repetitivo.

Metaprogramación con Swift Macros y ejemplos prácticos de código

Las Swift Macros, introducidas con Swift 5.9 y Xcode 15, suponen una revolución en la forma de escribir código Swift. Esta funcionalidad permite generar código en tiempo de compilación, eliminando el boilerplate sin renunciar a la seguridad de tipos estática. A diferencia de las macros del preprocesador de C, las Swift Macros son seguras en tipos, están integradas en el compilador y cuentan con soporte completo en las herramientas de desarrollo.

Lo que cubre esta guía

Esta guía explora la creación de Swift Macros de principio a fin: desde los conceptos fundamentales hasta implementaciones avanzadas, con ejemplos de código funcionales listos para adaptar a cualquier proyecto iOS.

Comprender los tipos de Swift Macros

Swift ofrece dos categorías principales de macros, cada una con casos de uso distintos. Las macros freestanding actúan de forma autónoma como expresiones o declaraciones, mientras que las macros attached se asocian a declaraciones existentes para modificarlas o enriquecerlas.

Macros freestanding: expresión y declaración

Las macros freestanding empiezan por el símbolo # y pueden devolver un valor (expresión) o crear nuevas declaraciones. A continuación, un ejemplo concreto de macro de expresión:

MacroUsage.swiftswift
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-03-11 10:30:45"

// Freestanding macro with arguments
let message = #stringify(1 + 2)
// Expansion → "1 + 2 = 3"

// Freestanding declaration macro - creates declarations
#makeCase("success", "failure", "pending")
// Expansion →
// case success
// case failure
// case pending

La diferencia esencial entre expresión y declaración reside en el resultado: una expresión produce un valor, mientras que una declaración produce código estructural (tipos, funciones, variables).

Macros attached: los cinco roles

Las macros attached empiezan por @ y se colocan delante de una declaración. Swift define cinco roles distintos para estas macros:

AttachedMacroRoles.swiftswift
// @attached(peer) - adds declarations at the same level
@AddAsync
func fetchUser(id: Int) -> User { ... }
// Expansion → adds func fetchUserAsync(id: Int) async -> User

// @attached(accessor) - adds getters/setters
@UserDefault("theme")
var currentTheme: String
// Expansion → adds get { UserDefaults.standard.string(...) }

// @attached(member) - adds members to a type
@AutoEquatable
struct Point {
    var x: Int
    var y: Int
}
// Expansion → adds static func == (lhs: Point, rhs: Point) -> Bool

// @attached(memberAttribute) - applies attributes to members
@CodableKeys
struct Config {
    var apiUrl: String
    var timeout: Int
}
// Expansion → adds @CodingKey("api_url") before apiUrl

// @attached(conformance) / @attached(extension) - adds conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → adds extension User: Hashable { ... }

Estos roles pueden combinarse para crear macros potentes capaces de transformar el código en varias dimensiones a la vez.

Configuración del proyecto para crear macros

La creación de Swift Macros requiere un Swift Package con una estructura concreta. El paquete depende de swift-syntax, la biblioteca oficial para manipular código Swift como un árbol de sintaxis abstracta (AST).

Estructura de Package.swift

Package.swiftswift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        // Library exposing macros to the main project
        .library(
            name: "MyMacros",
            targets: ["MyMacros"]
        ),
        // Executable for testing macros
        .executable(
            name: "MyMacrosClient",
            targets: ["MyMacrosClient"]
        )
    ],
    dependencies: [
        // Required dependency for macros
        .package(
            url: "https://github.com/apple/swift-syntax.git",
            from: "509.0.0"
        )
    ],
    targets: [
        // Compiler plugin containing implementation
        .macro(
            name: "MyMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        // Target exposing macro declarations
        .target(
            name: "MyMacros",
            dependencies: ["MyMacrosPlugin"]
        ),
        // Test client
        .executableTarget(
            name: "MyMacrosClient",
            dependencies: ["MyMacros"]
        ),
        // Unit tests
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
            ]
        )
    ]
)

Esta configuración separa con claridad las declaraciones de las macros (lo que ve el código cliente) de su implementación (que se ejecuta en tiempo de compilación).

Organización recomendada

Se necesitan al menos tres archivos: MyMacros.swift para las declaraciones, MyMacrosPlugin.swift para las implementaciones y MyMacrosTests.swift para los tests. Esta separación facilita el mantenimiento.

Crear una macro de expresión

Las macros de expresión generan un valor utilizable directamente en el código. A continuación se muestra cómo crear una macro #unwrap que desempaqueta un opcional con un mensaje de error personalizado que incluye el nombre de la variable.

Declaración de la macro

MyMacros.swiftswift
import Foundation

/// Macro that unwraps an optional with an explicit error message
/// Usage: let value = #unwrap(optionalValue)
/// Expansion: guard let optionalValue else { fatalError("...") }; optionalValue
@freestanding(expression)
public macro unwrap<T>(_ value: T?) -> T = #externalMacro(
    module: "MyMacrosPlugin",
    type: "UnwrapMacro"
)

La firma declara que la macro recibe un opcional y devuelve el valor no opcional. #externalMacro apunta a la implementación dentro del plugin.

Implementación con swift-syntax

UnwrapMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct UnwrapMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Get the first argument passed to the macro
        guard let argument = node.argumentList.first?.expression else {
            throw MacroError.missingArgument
        }

        // Extract the variable name for the error message
        let variableName = argument.description.trimmingCharacters(
            in: .whitespacesAndNewlines
        )

        // Generate the expansion code
        // Uses an immediately-invoked closure to encapsulate the guard
        return """
            {
                guard let value = \(argument) else {
                    fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil")
                }
                return value
            }()
            """
    }
}

// Custom errors for macros
enum MacroError: Error, CustomStringConvertible {
    case missingArgument
    case invalidSyntax(String)

    var description: String {
        switch self {
        case .missingArgument:
            return "The macro requires an argument"
        case .invalidSyntax(let message):
            return "Invalid syntax: \(message)"
        }
    }
}

El método expansion recibe el nodo del AST que representa la llamada a la macro junto con el contexto de compilación. Devuelve un ExprSyntax que contiene el código generado.

Registro del plugin

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    // List all macros provided by this plugin
    let providingMacros: [Macro.Type] = [
        UnwrapMacro.self,
        // Add other macros here
    ]
}

Este punto de entrada informa al compilador sobre las macros disponibles en el plugin.

¿Listo para aprobar tus entrevistas de iOS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Crear una macro attached de tipo member

Las macros member añaden miembros (propiedades, métodos, tipos anidados) a un tipo existente. Aquí se muestra una macro @AutoInit que genera automáticamente un inicializador con todas las propiedades almacenadas.

Declaración e implementación completas

MyMacros.swiftswift
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AutoInitMacro"
)
AutoInitMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoInitMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify the macro is applied to a struct or class
        guard declaration.is(StructDeclSyntax.self) ||
              declaration.is(ClassDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AutoInit can only be applied to structs and classes"
            )
        }

        // Collect stored properties
        let properties = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { isStoredProperty($0) }

        // Generate initializer parameters
        let parameters = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
                  let type = binding.typeAnnotation?.type else {
                return nil
            }

            let name = identifier.identifier.text
            let typeName = type.description.trimmingCharacters(in: .whitespaces)

            // Check if the property has a default value
            if binding.initializer != nil {
                return "\(name): \(typeName) = \(binding.initializer!.value)"
            }
            return "\(name): \(typeName)"
        }

        // Generate assignments in the init body
        let assignments = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
                return nil
            }
            let name = identifier.identifier.text
            return "self.\(name) = \(name)"
        }

        // Build the complete initializer
        let initDecl: DeclSyntax = """
            public init(\(raw: parameters.joined(separator: ", "))) {
                \(raw: assignments.joined(separator: "\n        "))
            }
            """

        return [initDecl]
    }

    // Check if a variable is a stored property (not computed)
    private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool {
        guard let binding = variable.bindings.first else { return false }

        // A computed property has an accessor block with get/set
        if let accessor = binding.accessorBlock {
            // If it's a block with explicit accessors, it's computed
            if accessor.accessors.is(AccessorDeclListSyntax.self) {
                return false
            }
        }

        // let or var without accessor = stored property
        return true
    }
}

Uso de la macro AutoInit

UserModel.swiftswift
@AutoInit
struct User {
    let id: UUID
    var name: String
    var email: String
    var isActive: Bool = true
}

// Automatically generated code:
// public init(id: UUID, name: String, email: String, isActive: Bool = true) {
//     self.id = id
//     self.name = name
//     self.email = email
//     self.isActive = isActive
// }

// Usage
let user = User(id: UUID(), name: "Alice", email: "alice@example.com")
// isActive uses the default value

Esta macro elimina el boilerplate del inicializador, algo especialmente útil en modelos de datos con muchas propiedades.

Macro attached peer para generación async

Las macros peer añaden declaraciones al mismo nivel que la declaración anotada. A continuación se muestra una macro @AddAsync que genera la versión async de una función basada en completion handler.

MyMacros.swiftswift
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AddAsyncMacro"
)
AddAsyncMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AddAsyncMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify it's a function
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AddAsync requires a function"
            )
        }

        let functionName = funcDecl.name.text
        let asyncFunctionName = "\(functionName)Async"

        // Analyze parameters to find the completion handler
        let parameters = funcDecl.signature.parameterClause.parameters

        // Filter parameters (exclude completion handler)
        var regularParams: [String] = []
        var completionType: String? = nil

        for param in parameters {
            let paramType = param.type.description

            // Detect a completion handler (closure with Result or simple value)
            if paramType.contains("->") && paramType.contains("Void") {
                // Extract the return type from completion
                completionType = extractCompletionReturnType(from: paramType)
            } else {
                let paramName = param.firstName.text
                let paramSecondName = param.secondName?.text
                let label = paramSecondName ?? paramName
                regularParams.append("\(paramName): \(paramType)")
            }
        }

        guard let returnType = completionType else {
            throw MacroError.invalidSyntax(
                "No completion handler found"
            )
        }

        // Generate arguments for internal call
        let callArgs = parameters.dropLast().map { param in
            let name = param.firstName.text
            return "\(name): \(name)"
        }.joined(separator: ", ")

        // Generate the async function
        let asyncFunc: DeclSyntax = """
            func \(raw: asyncFunctionName)(\(raw: regularParams.joined(separator: ", "))) async throws -> \(raw: returnType) {
                try await withCheckedThrowingContinuation { continuation in
                    \(raw: functionName)(\(raw: callArgs.isEmpty ? "" : callArgs + ", ")completion: { result in
                        switch result {
                        case .success(let value):
                            continuation.resume(returning: value)
                        case .failure(let error):
                            continuation.resume(throwing: error)
                        }
                    })
                }
            }
            """

        return [asyncFunc]
    }

    // Extract return type from a Result type
    private static func extractCompletionReturnType(from type: String) -> String {
        // Simplified pattern - in production, use the AST
        if let match = type.range(of: #"Result<([^,]+)"#, options: .regularExpression) {
            var result = String(type[match])
            result = result.replacingOccurrences(of: "Result<", with: "")
            return result.trimmingCharacters(in: .whitespaces)
        }
        return "Void"
    }
}

Demostración de la macro AddAsync

NetworkService.swiftswift
class NetworkService {
    @AddAsync
    func fetchUser(
        id: Int,
        completion: @escaping (Result<User, Error>) -> Void
    ) {
        // Implementation with callback
        URLSession.shared.dataTask(with: URL(string: "/users/\(id)")!) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                let user = try? JSONDecoder().decode(User.self, from: data)
                completion(.success(user!))
            }
        }.resume()
    }

    // Automatically generates:
    // func fetchUserAsync(id: Int) async throws -> User {
    //     try await withCheckedThrowingContinuation { continuation in
    //         fetchUser(id: id, completion: { result in
    //             switch result {
    //             case .success(let value):
    //                 continuation.resume(returning: value)
    //             case .failure(let error):
    //                 continuation.resume(throwing: error)
    //             }
    //         })
    //     }
    // }
}

// Modern usage with async/await
let user = try await networkService.fetchUserAsync(id: 42)
Limitaciones de las macros peer

El nombre de la función generada debe declararse en names: del atributo @attached. Aquí, suffixed(Async) indica que la función generada llevará el sufijo "Async" añadido al nombre original.

Pruebas unitarias de las macros

Probar las macros es fundamental, ya que generan código que después se compilará. Swift proporciona SwiftSyntaxMacrosTestSupport para facilitar este tipo de pruebas.

MyMacrosTests.swiftswift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin

final class MyMacrosTests: XCTestCase {

    // Dictionary of macros to test
    let testMacros: [String: Macro.Type] = [
        "unwrap": UnwrapMacro.self,
        "AutoInit": AutoInitMacro.self,
        "AddAsync": AddAsyncMacro.self
    ]

    func testUnwrapMacroExpansion() throws {
        assertMacroExpansion(
            """
            let value = #unwrap(optionalString)
            """,
            expandedSource: """
            let value = {
                guard let value = optionalString else {
                    fatalError("Failed to unwrap 'optionalString' - value was nil")
                }
                return value
            }()
            """,
            macros: testMacros
        )
    }

    func testAutoInitMacroWithStruct() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Point {
                let x: Int
                var y: Int
            }
            """,
            expandedSource: """
            struct Point {
                let x: Int
                var y: Int

                public init(x: Int, y: Int) {
                    self.x = x
                    self.y = y
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitWithDefaultValues() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Config {
                var timeout: Int = 30
                var retryCount: Int
            }
            """,
            expandedSource: """
            struct Config {
                var timeout: Int = 30
                var retryCount: Int

                public init(timeout: Int = 30, retryCount: Int) {
                    self.timeout = timeout
                    self.retryCount = retryCount
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitFailsOnEnum() throws {
        assertMacroExpansion(
            """
            @AutoInit
            enum Status {
                case active
            }
            """,
            expandedSource: """
            enum Status {
                case active
            }
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "@AutoInit can only be applied to structs and classes",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMacros
        )
    }
}

Las pruebas verifican la correcta expansión del código y los mensajes de error apropiados ante un uso indebido.

¿Listo para aprobar tus entrevistas de iOS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Macro avanzada: property wrapper observable

Esta macro combina varios roles para crear un sistema de observación de propiedades con notificaciones automáticas.

MyMacros.swiftswift
/// Adds automatic property change observation
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "ObservableMacro"
)
ObservableMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

// Implements both roles: accessor and peer
public enum ObservableMacro {}

extension ObservableMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [AccessorDeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"

        // Generate get and set accessors
        let getter: AccessorDeclSyntax = """
            get {
                access(keyPath: \\.\(raw: name))
                return \(raw: storageName)
            }
            """

        let setter: AccessorDeclSyntax = """
            set {
                withMutation(keyPath: \\.\(raw: name)) {
                    \(raw: storageName) = newValue
                }
            }
            """

        return [getter, setter]
    }
}

extension ObservableMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
              let type = binding.typeAnnotation?.type else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"
        let typeName = type.description

        // Generate private storage property
        let initializer = binding.initializer.map { " \($0)" } ?? ""
        let storageDecl: DeclSyntax = """
            private var \(raw: storageName): \(raw: typeName)\(raw: initializer)
            """

        return [storageDecl]
    }
}

Uso del patrón Observable

ViewModel.swiftswift
@Observable
class UserViewModel {
    @Observable var name: String = ""
    @Observable var age: Int = 0
    @Observable var isActive: Bool = true

    // Generated code for each property:
    // private var _name: String = ""
    // var name: String {
    //     get {
    //         access(keyPath: \.name)
    //         return _name
    //     }
    //     set {
    //         withMutation(keyPath: \.name) {
    //             _name = newValue
    //         }
    //     }
    // }
}

Apple aplica este mismo patrón en el nuevo framework Observation desde Swift 5.9.

Depurar e inspeccionar las macros

Xcode ofrece varias herramientas para depurar las macros y entender el código generado.

Expansión en Xcode

DebuggingMacros.swiftswift
// Right-click on macro call → "Expand Macro"
// Displays generated code inline

@AutoInit
struct Product {
    let id: UUID
    var name: String
    var price: Decimal
}

// To see the expansion:
// 1. Right-click on @AutoInit
// 2. Select "Expand Macro"
// 3. Generated code displays inline for inspection and debugging

Logging durante el desarrollo

DebugMacro.swiftswift
public struct DebugMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Print the node's AST to understand the structure
        print("=== DEBUG MACRO ===")
        print("Node: \(node)")
        print("Arguments: \(node.argumentList)")

        // Complete dump of the syntax tree
        dump(node)

        // Continue with normal expansion
        return "42"
    }
}

Explorar el AST con swift-ast-explorer

La herramienta en línea swift-ast-explorer.com permite visualizar el árbol de sintaxis de cualquier código Swift. Resulta imprescindible para entender cómo recorrer los nodos del AST al implementar una macro.

Buenas prácticas para Swift Macros

Crear macros mantenibles requiere seguir ciertas convenciones y evitar errores habituales.

Validación y mensajes de error

ValidationBestPractices.swiftswift
public struct ValidatedMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // ✅ Validate usage context
        guard declaration.is(StructDeclSyntax.self) else {
            // ✅ Clear error messages with possible localization
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "invalid-target",
                        message: "This macro can only be applied to structs",
                        severity: .error
                    )
                )
            )
            return []
        }

        // ✅ Check required arguments
        guard let arguments = node.arguments else {
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "missing-args",
                        message: "Required arguments missing",
                        severity: .error
                    )
                )
            )
            return []
        }

        // Implementation...
        return []
    }
}

// Structure for diagnostic messages
struct MacroDiagnosticMessage: DiagnosticMessage {
    let id: String
    let message: String
    let severity: DiagnosticSeverity

    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: id)
    }
}

Generar código legible

ReadableCodeGeneration.swiftswift
// ❌ Hard-to-read generated code
let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}"

// ✅ Properly formatted generated code
let goodCode: DeclSyntax = """
    public init(
        a: Int,
        b: String,
        c: Bool
    ) {
        self.a = a
        self.b = b
        self.c = c
    }
    """

El código generado debe resultar tan legible como el escrito a mano, ya que las personas que desarrollen lo inspeccionarán mediante "Expand Macro".

Conclusión

Las Swift Macros son una herramienta potente para eliminar boilerplate sin renunciar a la seguridad de tipos estática. Esta tecnología permite:

Puntos clave:

  • ✅ Dos categorías: freestanding (#) y attached (@)
  • ✅ Cinco roles attached: peer, accessor, member, memberAttribute, conformance
  • ✅ Implementación mediante swift-syntax y manipulación del AST
  • ✅ Pruebas obligatorias con SwiftSyntaxMacrosTestSupport
  • ✅ Paquete separado obligatorio para las implementaciones
  • ✅ Depuración mediante "Expand Macro" en Xcode
  • ✅ Mensajes de error explícitos esenciales para la experiencia de quien desarrolla

Las Swift Macros resultan especialmente útiles para generar conformidades (Equatable, Codable), crear property wrappers avanzados y modernizar APIs basadas en callbacks hacia async/await.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#swift
#ios
#macros
#metaprogramming
#swift-syntax

Compartir

Artículos relacionados