Autor: admin

  • Stopping DoS Attacks: Vapor + Redis

    Stopping DoS Attacks: Vapor + Redis

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

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

    Hello Vapor Server

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

    vapor new VaporDoS

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

    Screenshot

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Build docker image and launch container by typing;

    docker composeup --build
    Screenshot

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

    Screenshot

    Configure Redis for avoiding DoS attacks

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

    First step is adding Redis library in Package.swift:

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

    Next step is configure Redis in configure.swift:

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

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

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

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

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

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

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

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

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

    Rebuild the image and launch the container:

    Screenshot

    Call /hello endpoint 3 times:

    Screenshot

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

    Conclusions

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

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

    References

  • Customizing Vapor Server Configurations

    Customizing Vapor Server Configurations

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

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

    Hello Vapor Server

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

    vapor new HelloServer

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

    Screenshot

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

    Open routes.swift file:

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

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

    Custom Configuration variables

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

    docker build -t hello-vapor .

    Finally we will run the container:

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

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

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

    curl http://localhost:8080/hello

    When we get back to server terminal window:

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

    Secrets

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

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

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

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

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

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

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

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

    Update endpoint code for also fetching SECRET:

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

    Build a the new docker image:

    docker build -t hello-vapor .

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

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

    Server iss ready again:

    Calling endpoint again:

     

    curl http://localhost:8080/hello

    Secreta is now fetched by the endpoint.

    Conclusions

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

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

    References

  • Protect sensitive information in SwiftUI

    Protect sensitive information in SwiftUI

    The goal of this post is to present some techniques for obfuscating or preventing easy access to highly sensitive information, such as account or credit card numbers.

    Allow copy

    The first thing to clarify is which pieces of information can be copied and which cannot. This behavior is controlled by the .textSelection() modifier.

    struct SensitiveCard: View {
        let title: String
        let primary: String
        let secondary: String
        let icon: String
        let isSelectable: Bool
    
        var body: some View {
            VStack(alignment: .leading, spacing: 12) {
                HStack(spacing: 12) {
                    Image(systemName: icon)
                        .font(.title2)
                        .padding(10)
                        .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14))
                    Text(title)
                        .font(.title3.weight(.semibold))
                    Spacer()
                }
                Text(primary)
                    .font(.system(.title2, design: .monospaced).weight(.semibold))
                    .conditionalTextSelection(isSelectable)
                Text(secondary)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            .padding(18)
            .background(
                RoundedRectangle(cornerRadius: 22, style: .continuous)
                    .fill(.regularMaterial)
                    .shadow(radius: 12, y: 6)
            )
        }
    }
    
    
    struct ConditionalTextSelection: ViewModifier {
        let enable: Bool
        
        func body(content: Content) -> some View {
            if enable {
                content.textSelection(.enabled)
            } else {
                content
            }
        }
    }
    
    extension View {
        func conditionalTextSelection(_ enable: Bool) -> some View {
            self.modifier(ConditionalTextSelection(enable: enable))
        }
    }

    In this example, we’ve chosen to make the IBAN copyable, while the card number remains restricted.

    struct ContentView: View {
        @State private var cardNumber = "1234 5678 9012 3456"
        @State private var cvv = "123"
        @State private var iban = "ES12 3456 7890 1234 5678 9012"
        @State private var textSelectability: TextSelectability = .disabled
    
        var body: some View {
            ScrollView {
                VStack(alignment: .leading, spacing: 24) {
                    
                    SensitiveCard(title: "IBAN",
                                  primary: iban,
                                  secondary: "Demo Bank",
                                  icon: "building.columns.fill",
                                  isSelectable: true)
                    
                    SensitiveCard(title: "Card",
                                  primary: cardNumber,
                                  secondary: "CVV \(cvv)",
                                  icon: "creditcard.fill",
                                  isSelectable: false)
    
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Safety measures:")
                            .font(.title3.bold())
                        Text("• Avoid sharing screenshots.\n• Enable Face ID for showing sensitive information.")
                            .foregroundStyle(.secondary)
                    }
                }
                .padding(24)
            }
        }
    }

    Run the app and long-press the IBAN code. A contextual pop-up will appear — select “Copy.” Then switch to the Notes app and paste it. You’ll notice that the same operation cannot be performed with the card information.

    Privacy button

    A measure that can help build user trust is to provide a button that hides sensitive information:

    import SwiftUI
    
    struct ContentView: View {
        .....
        var body: some View {
            ScrollView {
                VStack(alignment: .leading, spacing: 24) {
                    ....
                    Button {
                        // Ejemplo: regenerar/limpiar datos
                        cardNumber = "•••• •••• •••• ••••"
                        cvv = "•••"
                        iban = "ES•• •••• •••• •••• •••• ••••"
                    } label: {
                        Label("Mask manually", systemImage: "eye.trianglebadge.exclamationmark.fill")
                    }
                    .buttonStyle(.borderedProminent)
                    .controlSize(.large)
                }
                .padding(24)
            }
        }
    }

    When changes are deployed:

    review

    Detect app activity

    An alternative to using a button, or a complementary feature, is detecting changes in the app’s activity — for example, when the app is moved to the background:

    import SwiftUI
    import UIKit
    
    struct ContentView: View {
        @Environment(\.scenePhase) private var scenePhase
        @State private var privacyMask = false
    ...
    
        var body: some View {
            ProtectedView {
                SensitiveView(hidden: $showScreenshotAlert)
            }
            .blur(radius: privacyMask ? 28 : 0)
            .overlay {
                if privacyMask {
                    ZStack {
                        Color.black.opacity(0.6).ignoresSafeArea()
                        VStack(spacing: 12) {
                            Image(systemName: "eye.slash.fill")
                                .font(.system(size: 36, weight: .semibold))
                            Text("Content hidden whilst app is not active")
                                .multilineTextAlignment(.center)
                                .font(.headline)
                        }
                        .foregroundColor(.white)
                        .padding()
                    }
                    .accessibilityHidden(true)
                }
            }
            .onChange(of: scenePhase) { phase in
                privacyMask = (phase != .active)
            }
            ...
    }

    Deploy and move app to background:

    review2

    Detect screenshot action

    An important point I discovered during this investigation is that, although the app can detect when a screenshot is taken — which sounds good — the screenshot is still captured anyway.

    My recommendation in that case would be to invalidate or regenerate the information if temporary keys are involved. Depending on the scenario, you could also notify the backend services that a screenshot has been taken.

    The code for detecting a screenshot is as follows:

    struct ContentView: View {
    ...
        @State private var showScreenshotAlert = false
    
        var body: some View {
            ProtectedView {
                SensitiveView(hidden: $showScreenshotAlert)
            }
           ...
            .onReceive(NotificationCenter.default.publisher(
                for: UIApplication.userDidTakeScreenshotNotification)) { _ in
                    showScreenshotAlert = true
                }
            .alert("Screenshot detected",
                   isPresented: $showScreenshotAlert) {
                Button("OK", role: .cancel) {}
            } message: {
                Text("For security, sensitive content is hidden when the screen is being captured.")
            }
        }
    }

    Ofuscate on Recording screen

    The final measure is to apply obfuscation when a screen recording is in progress:

    struct ProtectedView<Content: View>: View {
        @State private var isCaptured = UIScreen.main.isCaptured
        @ViewBuilder var content: () -> Content
    
        var body: some View {
            content()
                .blur(radius: isCaptured ? 25 : 0)
                .overlay {
                    if isCaptured {
                        ZStack {
                            Color.black.opacity(0.65).ignoresSafeArea()
                            VStack(spacing: 12) {
                                Image(systemName: "lock.fill")
                                    .font(.system(size: 32, weight: .bold))
                                Text(String(localized: "protected.overlay.title",
                                            defaultValue: "Content hidden while the screen is being captured"))
                                    .multilineTextAlignment(.center)
                                    .font(.headline)
                                    .padding(.horizontal)
                            }
                            .foregroundColor(.white)
                        }
                        .accessibilityHidden(true)
                    }
                }
                .onReceive(NotificationCenter.default.publisher(
                    for: UIScreen.capturedDidChangeNotification)) { _ in
                        isCaptured = UIScreen.main.isCaptured
                    }
        }
    }

    When a screen recording session is active and the user switches to our privacy app, the app detects the recording and can respond by displaying an overlay.

    recording

    Conclusions

    It’s not possible to achieve complete protection against data forgery or privacy breaches, but the more countermeasures you apply, the better your security becomes. That’s what I wanted to demonstrate in this post.

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

    References

  • Rules to write Clean Code in Swift

    Rules to write Clean Code in Swift

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

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

    Clean Code

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

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

    SOC – Separation Of Concepts

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

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

    Where’s the SoC?

    • Model (Post) → defines the data only.

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

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

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

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

    DRY – Do not repeat yourself

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

    ❌ Without DRY (duplicated logic)

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

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

    ✅ With DRY (reusable abstraction)

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

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

    KISS-Keep it simple stupid

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

    ❌ Not KISS (too complex for a simple counter)

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

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


    ✅ KISS (straightforward and clear)

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

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

    DYC-Document your code

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

    ✅ Example in Swift using DYC

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

    Why this follows DYC

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

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

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

    YAGNI-You’re not going to need it

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


    ❌ Violating YAGNI (adding unnecessary features)

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

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


    ✅ Following YAGNI (keep it simple until needed)

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

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

    LoD-Law of Demeter

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

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

    ❌ Violating the Law of Demeter

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

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


    ✅ Following the Law of Demeter

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

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

    Lesson:

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

    • ✅ Provide methods that expose the needed behavior directly.

    Conclusions

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

  • Xcode 16: Alternate App Icons in iOS

    Xcode 16: Alternate App Icons in iOS

    Changing an iOS app icon can create both user and business value by offering personalization, seasonal or event-based marketing opportunities, and fresh ways to engage users without requiring an App Store resubmission. It allows apps to feel dynamic and relevant—whether through holiday icons, unlockable rewards for loyal users, or collectible designs tied to achievements. This flexibility also supports monetization strategies (e.g., premium icons for subscribers), strengthens community connection (such as sports teams or fan apps), and enables practical use cases like localization or representing different brands. In short, dynamic icons turn the app’s presence on the home screen into a living extension of the product experience. In this post, we’ll build a sample iOS app that demonstrates how to implement app icon switching.

    Project Setup

    First step is create a new App Icon:

    After adding a second app icon asset, select your app target, open Build Settings, and set Include All App Icon Assets to Yes. Note: verify your Xcode version—I’m using Xcode 16, and earlier versions configure this slightly differently.

    At that point, you might be tempted to create an app with a live icon—like Calendar or Clock.

     
     

    Unfortunately, third-party apps can’t do this—dynamic icons are a system-only capability enabled by Apple’s private entitlements. For everyone else, the app icon is a static resource in the signed bundle and can only be swapped for another prepackaged icon via setAlternateIconName(_:).

    App Icon Manager

    The component that where all icon changeability will be centered is following:

    import UIKit
    
    enum AppIconName: String {
        case primary = ""          // Default icon
        case dark = "AppIconB"     // exact name for the alternative icon (no extension)
    }
    
    enum AppIconManager {
        static var supportsAlternateIcons: Bool {
            UIApplication.shared.supportsAlternateIcons
        }
    
        static var currentAlternateIconName: String? {
            UIApplication.shared.alternateIconName
        }
    
        static func setAppIcon(_ icon: AppIconName, completion: ((Error?) -> Void)? = nil) {
            guard supportsAlternateIcons else {
                completion?(NSError(domain: "AppIcon", code: -1, userInfo: [NSLocalizedDescriptionKey: "This device does not support alternative icons"]))
                return
            }
    
            let nameToSet: String? = (icon == .primary) ? nil : icon.rawValue
    
            if UIApplication.shared.alternateIconName == nameToSet {
                completion?(nil)
                return
            }
    
            UIApplication.shared.setAlternateIconName(nameToSet) { error in
                completion?(error)
            }
        }
    }
    

    The AppIconName enum represents the available icons (the default primary and an alternate one named AppIconB). The AppIconManager enum provides helper properties and a method: it checks if the device supports alternate icons, retrieves the currently active icon, and lets you switch to a new icon with setAppIcon. If the device doesn’t support icon changes or if the requested icon is already active, it gracefully exits, otherwise it uses UIApplication.shared.setAlternateIconName to apply the new icon and calls the completion handler with either an error or success.

    View

    This SwiftUI ContentView provides a simple UI that lets users toggle between the app’s default and alternative icons. It shows a large sun or moon symbol depending on the selected icon, and includes a switch labeled “Use alternative icon” that triggers AppIconManager to change the app icon when toggled. If the device doesn’t support alternate icons, the toggle is disabled and a warning message is displayed. The view also shows which icon is currently active, updates the user with a status message after each change (or error), and ensures the correct icon is applied when the view first appears.

    struct ContentView: View {
        @AppStorage("useAltIcon") private var useAltIcon: Bool = false
        @State private var statusMsg: String = ""
    
        var body: some View {
            VStack(spacing: 20) {
                Image(systemName: useAltIcon ? "moon.fill" : "sun.max.fill")
                    .font(.system(size: 56))
                    .symbolRenderingMode(.hierarchical)
    
                Toggle("Use alternative icon", isOn: $useAltIcon)
                    .onChange(of: useAltIcon) { _, newValue in
                        AppIconManager.setAppIcon(newValue ? .dark : .primary) { err in
                            if let err = err {
                                statusMsg = "It was not possible replacie icon: \(err.localizedDescription)"
                                // revertir el toggle si algo falla
                                useAltIcon.toggle()
                            } else {
                                statusMsg = newValue ? "Alternative icon activated." : "Default icon restored."
                            }
                        }
                    }
                    .disabled(!AppIconManager.supportsAlternateIcons)
                    .padding(.horizontal)
    
                if !AppIconManager.supportsAlternateIcons {
                    Text("This device does not support alternative icons.")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
    
                if let current = AppIconManager.currentAlternateIconName {
                    Text("Current icon: \(current)")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                } else {
                    Text("Curent icon: default")
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                }
    
                if !statusMsg.isEmpty {
                    Text(statusMsg)
                        .font(.footnote)
                        .foregroundStyle(.secondary)
                        .multilineTextAlignment(.center)
                        .padding(.top, 4)
                }
    
                Spacer()
            }
            .padding()
            .onAppear {
                let shouldBeAlt = useAltIcon
                AppIconManager.setAppIcon(shouldBeAlt ? .dark : .primary) { _ in }
            }
        }
    }

    Deploy the code on a simulator to validate its behavior (or real device):

    review

    The app icon is being replaced by the app itself.

    Conclusions

    In this post, I’ve shared a quick and easy feature you can implement to make your app more engaging. You can find source code that we have used for conducting this post in following GitHub repository.

    References

  • iOS Dev between Combine and Delegate/Closure

    iOS Dev between Combine and Delegate/Closure

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

    The dilemma

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

    Here’s what they share in common:

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

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

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

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

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

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

    The rule of thumb

    Use Combine when…

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

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

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

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

    Use delegates/closures when…

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

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

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

    Examples

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

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

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

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

    Closure variant (simple one-shot completion):

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

    Quick decision checklist

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

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

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

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

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

    Bridge between them

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

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

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

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

    Example: wrapping a closure in a publisher

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

    Conclusions

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

    References

    • Combine

      Apple Developer Documentation

  • Goodbye Raw Strings, Hello swift-tagged

    Goodbye Raw Strings, Hello swift-tagged

    There is a subtle but common problem in Swift development: relying on raw types like String or UUID for identifiers leads to fragile code where values can be accidentally swapped or misused without the compiler noticing. By explaining how swift-tagged introduces zero-cost, strongly typed wrappers, I wanted to show you how other iOS developers how to prevent whole categories of bugs at compile time, while keeping APIs clean, expressive, and fully compatible with Codable, Hashable, and other Swift protocols. It’s a practical, easy-to-adopt tool that makes codebases more robust, and many developers may not even realize how much type safety they’re leaving on the table until they see a real-world example.

    The problem

    The following code is syntactically correct, but semantically wrong.

        struct UserRaw { let id: UUID }
        struct ProductRaw { let id: UUID }
    
        func registerPurchaseRaw(userID: UUID, productID: UUID) {
            log("✅ Purchase registered (RAW): user=\(userID) product=\(productID)")
        }
    
        func demoRaw() {
            log("— RAW demo —")
            let rawUser = UserRaw(id: UUID())
            let rawProduct = ProductRaw(id: UUID())
    
            // ❌ Compiles, BUT CODE SEMANTICALLY IS WRONG (crossed arguments)
            registerPurchaseRaw(userID: rawProduct.id, productID: rawUser.id)
            log("")
        }

    This code defines two structs, UserRaw and ProductRaw, each holding an id of type UUID, and a function registerPurchaseRaw(userID:productID:) that logs a message about a registered purchase. In demoRaw(), it creates a user and a product, then mistakenly calls registerPurchaseRaw with the arguments swapped (userID is given the product’s ID and productID is given the user’s ID). The key issue is that both IDs are plain UUIDs, so the compiler cannot distinguish between them—this compiles without error even though it is logically wrong. The problem is a lack of type safety, which can lead to subtle bugs where mismatched identifiers are passed to functions unnoticed until runtime.

    swift-tagged library

    The swift-tagged library is a lightweight Swift package from Point-Free that lets you create strongly typed wrappers around existing primitive types like String, Int, or UUID. Instead of passing around raw values (e.g. using plain UUID for both UserID and OrderID), you can “tag” them with distinct types so the compiler enforces correct usage—preventing accidental mix-ups that would otherwise compile but cause logic bugs. It’s a zero-cost abstraction, meaning it adds no runtime overhead, and it integrates seamlessly with protocols like Codable, Hashable, and Equatable. In practice, swift-tagged helps make Swift code more expressive, self-documenting, and safer, especially when modeling identifiers and domain-specific values in iOS or server-side Swift apps.

    This is new proposal with swift-tagged library:

        struct UserTag {}
        struct ProductTag {}
    
        typealias UserID = Tagged<UserTag, UUID>
        typealias ProductID = Tagged<ProductTag, UUID>
    
        struct User {
            let id: UserID
        }
        struct Product {
            let id: ProductID
        }
    
        func registerPurchase(userID: UserID, productID: ProductID) {
            log("✅ Purchase registered (Tagged): user=\(userID) product=\(productID)")
        }
    
        func demoTagged() {
            log("— Tagged demo —")
            let user = User(id: UserID(UUID()))
            let product = Product(id: ProductID(UUID()))
            registerPurchase(userID: user.id, productID: product.id)
    
            // ❌ This no longer compiles (type mismatch): // registerPurchase(userID: product.id, productID: user.id
            registerPurchase(userID: product.id, productID: user.id)
            log("")
        }

    Now at compile time, semantic error is being detected:

    Codable types

    swift-tagged works with Codable types because its Tagged wrapper is designed to forward encoding and decoding responsibilities to the underlying raw type (such as String, Int, or UUID) that already conforms to Codable. This means when you use a Tagged<User, UUID> as a property in a model, Swift’s Codable machinery simply encodes or decodes the inner UUID as usual, while still preserving the type-safe distinction at compile time. As a result, you get the safety of strongly typed identifiers without having to write custom Codable implementations or change how your models interact with JSON or other encoded data.

            log("— Codable + JSON —")
    
            let user = User(id: UserID(UUID()))
            let product = Product(id: ProductID(UUID()))
            let request = PurchaseRequest(userID: user.id, productID: product.id)
    
            // Encode → JSON
            do {
                let encoder = JSONEncoder()
                encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
                let jsonData = try encoder.encode(request)
                if let jsonString = String(data: jsonData, encoding: .utf8) {
                    log("📤 JSON sent to server:")
                    log(jsonString)
                }
            } catch {
                log("Encoding error:", error.localizedDescription)
            }
    
            // Decode ← JSON
            let jsonInput = """
            {
                "userID": "\(UUID())",
                "productID": "\(UUID())"
            }
            """.data(using: .utf8)!
    
            do {
                let decoded = try JSONDecoder().decode(PurchaseRequest.self, from: jsonInput)
                log("📥 JSON received and decoded to Swift struct:")
                log("userID: \(decoded.userID)")
                log("productID: \(decoded.productID)")
            } catch {
                log("Decoding error:", error.localizedDescription)
            }
    
            log("")

    This code demonstrates how a PurchaseRequest that uses swift-tagged identifiers can be seamlessly encoded to and decoded from JSON. First, it creates a User and a Product, builds a PurchaseRequest with their strongly typed IDs, and then uses JSONEncoder to serialize it into a nicely formatted JSON string, simulating data being sent to a server. Next, it constructs a JSON string containing new random UUIDs for userID and productID, converts it to Data, and decodes it back into a PurchaseRequest instance with JSONDecoder. The output shows that although the code benefits from type safety at compile time, the wrapped values still encode and decode just like plain UUIDs, ensuring compatibility with standard JSON APIs.

    Hashable

    Yes—swift-tagged works with Hashable types because its Tagged<Tag, RawValue> wrapper automatically conforms to Hashable whenever the underlying RawValue does (e.g., UUID, String, Int). This means tagged IDs like UserID or ProductID can be used directly in Sets to enforce uniqueness, as keys in Dictionarys, or inside other Hashable models without extra boilerplate. In practice, you get the benefits of type safety and domain clarity while still leveraging Swift’s built-in hashing behavior, all with zero runtime overhead.

            // ---------------------------------------------------------
            // 🔢 4. Using swift-tagged with Hashable collections
            // ---------------------------------------------------------
    
            // Sets of tagged IDs
            let user = User(id: UserID(UUID()))
            var seenUsers = Set<UserID>()
            seenUsers.insert(user.id)                 // from earlier code
            seenUsers.insert(UserID(UUID()))          // a different user
            seenUsers.insert(user.id)                 // duplicate; Set ignores it
    
            log("👥 Seen users (unique count): \(seenUsers.count)")
    
            // Dictionaries with tagged IDs as keys
            let product = Product(id: ProductID(UUID()))
            var productStock: [ProductID: Int] = [:]
            productStock[product.id] = 10             // from earlier code
            let anotherProductID = ProductID(UUID())
            productStock[anotherProductID] = 5
    
            log("📦 Product stock:")
            for (pid, qty) in productStock {
                log(" - \(pid): \(qty)")
            }
    
            // Using tagged IDs inside Hashable models
            struct CartItem: Hashable {
                let productID: ProductID
                let quantity: Int
            }
    
            var cart = Set<CartItem>()
            cart.insert(CartItem(productID: product.id, quantity: 1))
            cart.insert(CartItem(productID: product.id, quantity: 1)) // duplicate CartItem; Set ignores it
            cart.insert(CartItem(productID: product.id, quantity: 2)) // different (quantity), so distinct
            cart.insert(CartItem(productID: anotherProductID, quantity: 1))
    
            log("🛒 Cart unique items: \(cart.count)")

    This code shows how swift-tagged identifiers can be used in Swift collections that rely on Hashable. First, it creates a Set<UserID> and demonstrates that inserting the same tagged ID twice does not create duplicates, while a new tagged ID is treated as unique. Next, it builds a dictionary [ProductID: Int] to associate stock counts with product IDs, proving that tagged IDs work seamlessly as dictionary keys. Finally, it defines a CartItem struct containing a tagged ProductID and makes it Hashable, then inserts several items into a Set<CartItem>—duplicates with identical values collapse into one entry, while items that differ in quantity or product ID remain distinct. Overall, the snippet illustrates how swift-tagged provides type-safe IDs that integrate naturally with Set and Dictionary without extra work.

    Conclusions

    Swift’s capabilities are growing year by year. I’m not a big fan of using third-party libraries, but to avoid the problem presented here I would highly recommend this one.


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

    References

  • Dynamic Island: iOS Live Activities Guide

    Dynamic Island: iOS Live Activities Guide

    A Live Activity in iOS is a special type of interactive widget that displays real-time information from an app directly on the Lock Screen and, on iPhone 14 Pro models and later, in the Dynamic Island. They’re designed for short-lived, glanceable updates—like tracking a food delivery, following a sports score, or showing a running timer—so users don’t need to constantly reopen the app. Built with ActivityKit, Live Activities can be updated by the app itself or through push notifications, and they automatically end once the tracked task or event is complete.

    In this post, we’ll walk through an iOS app project that covers the entire flight reservation journey—from the moment your booking is confirmed to when your bags arrive at the baggage claim. At the end, you’ll find a link to the GitHub repository if you’d like to download the project and try it out yourself.

    Project Setup

    To carry out this project, we started with a blank iOS app template containing two targets: one for the main app itself and another for a Widget Extension.

    Go to the Signing & Capabilities tab of your iOS app target and enable the Push Notifications capability.

    We need this because Live Activity state changes are triggered by push notifications. Next, update the Info.plist to support Live Activities:

    Regarding Widget Extension Target, no extra setup is required. When we review project explorer we will face 2 packages:

    Payload Generator is a small command-line tool that prints JSON payloads to the console, ready to be pasted directly into the push notifications console. LiveActivityData contains all data structures (and sample data) related to Live Activities. Including them in a package allows the module to be imported by the iOS app, the Widget Extension, and the Payload Generator

    Up to this point is all you need to know about the project, deploy the app on a real device:

    Screenshot

    In addition to handling Live Activity state changes through push notifications, we’ll also manage them internally from the app itself by triggering updates with a button.

    Create Booking

    For creating a new booking, we will create internally from the app just pressing the corresponding button. The app follows MVVM architecture pattern and the method for handling that in the View Model is following:

        func startActivity(initialState: FlightActivityAttributes.ContentState) {
            let attrs = FlightActivityAttributes.bookingActivity
            let content = ActivityContent(state: initialState, staleDate: nil)
    
            do {
                currentActivity = try Activity.request(
                    attributes: attrs,
                    content: content,
                    pushType: .token
                )
                refreshActivities()
            } catch {
                logger.error("Failed to start activity: \(error.localizedDescription, privacy: .public)")
            }
        }

    If we move out from the app we will see dynamic island (left) and block screen (right) presenting a Widget with following content:

    Screenshot
    Screenshot

    Running out of seats

    To let our customers know about seat availability for their booking, we’ll send a push notification to the app with the updated seat information. The first step is to open the Push Notifications console:

    Log in with your Apple Developer account and open the Push Notifications dashboard. Verify that you’ve selected the correct team and Bundle ID, then click Send and choose New.

    Log in with your Apple Developer account and open the Push Notifications dashboard. Make sure you’ve selected the correct team and Bundle ID. Then click Send and choose New. For Name, enter a descriptive label to help you recognize the purpose of this push notification. Next, under Recipient, paste the last hex code that appeared in the logging console.

    To generate the JSON payload for the push notification, we’ll use our command-line tool. Run the following command:

    $swift run PayloadGenerator 2

    Here, 2 generates a sample template showing 30% of the available seats.»

    On apns-push-type select liveactivity and paste previous generated download on payload:

    Press the Send button, and you’ll see the following content displayed on the device—both in the Dynamic Island and on the Lock Screen widget:»

    Checkin available

    A few weeks before a flight departs, airlines usually allow users to check in online. To generate the payload for this scenario, run:

    $swift run PayloadGenerator 3

    Here, 3 generates a sample template that enables the user to perform online check-in. In the Push Notifications dashboard, update the token, paste the payload, and send the notification. You should then see the following:

    When a push notification arrives, the Dynamic Island first appears in compact mode (left). If the user taps it, the Dynamic Island expands (center), and finally the widget is shown (right) when user blocks device. Notice that the widget displays a gradient background, while the Dynamic Island does not—this is because the Dynamic Island is designed to cover the area where the camera and sensors are physically located on the device screen.

    It’s important that the widget and the expanded Dynamic Island share the same composition to ensure maintainability and to simplify the addition or removal of new states. WidgeKit facilitates it by allowin developer implement it on the same class:

    struct BookingFlightLiveActivity: Widget {
        var body: some WidgetConfiguration {
            ActivityConfiguration(for: FlightActivityAttributes.self) { context in
                let attrs = context.attributes
                let state = context.state
    
                FlightWidgetView(attrs: attrs, state: state)
    
            } dynamicIsland: { context in
                let journey = context.attributes.journey
                let state = context.state
    
                return DynamicIsland {
                    DynamicIslandExpandedRegion(.leading) {
                        OriginView(
                            imageName: journey.imageName,
                            origin: journey.origin,
                            departure: state.departure,
                            flightState: state.flightState
                        )
                    }
    
                    DynamicIslandExpandedRegion(.trailing) {
                        DestinationView(
                            flightNumber: journey.flightNumber,
                            destination: journey.destination,
                            arrivalDateTime: state.arrivalDateTime,
                            flightState: state.flightState
                        )
                    }
    
                    DynamicIslandExpandedRegion(.center) {
                        CentralView(
                            departure: state.departure,
                            flightState: state.flightState
                        )
                    }
    
                    DynamicIslandExpandedRegion(.bottom) {
                        ExtraView(flightState: state.flightState)
                    }
                } compactLeading: {
                    CompactLeadingView(
                        origin: journey.origin,
                        destination: journey.destination,
                        flightNumber: journey.flightNumber,
                        flightState: state.flightState
                    )
                } compactTrailing: {
                    CompactTrailingView(
                        flightNumber: journey.flightNumber,
                        flightState: state.flightState
                    )
                } minimal: {
                    MinimalView()
                }
            }
            .supplementalActivityFamilies([.small, .medium])
        }
    }

    This Swift code defines a Live Activity widget called BookingFlightLiveActivity for an iOS flight booking app. It uses ActivityConfiguration to display real-time flight information on the Lock Screen and within the Dynamic Island. On the Lock Screen (FlightWidgetView), it shows booking attributes and state (such as departure, arrival, and flight status). For the Dynamic Island, it customizes different regions: the leading side shows the origin airport, the trailing side shows destination details, the center highlights departure and status, and the bottom provides extra information. It also specifies how the widget appears in compact leading, compact trailing, and minimal Dynamic Island modes. Additionally, it declares support for extra widget sizes (.small and .medium) through supplementalActivityFamilies; for example, .small is used to present the widget on Apple Watch.

    Another important detail is the context, which holds the presentation data. This is divided into two groups: attributes, which are fixed values (such as journey details), and state, which contains variable information that changes as the Live Activity progresses.

    Boarding

    Now, it gets time of boarding. At this stage we’re going to take a look at the command line tool that we have also developed and facilitates work for generating JSON payload for push notification. To generate the payload for this scenario, run:

    $swift run PayloadGenerator 4

    Here, 4 generates a sample payload template that enables the user be informed about the boarding gate.

    JSONPaload command line tool just parses input atttirbutes and executes its function associated:

    import ArgumentParser
    
    @main
    struct JSONPayload: ParsableCommand {
        @Argument(help: "Which step of the live activity cycle to generate as JSON")
        var step: Int
    
        @Flag(help: "Prints date in a human-readable style")
        var debug: Bool = false
    
        mutating func run() throws {
            let jsonString = switch step {
            case 1: try bookedFlight(debug: debug)
            case 2: try bookedFlight30Available(debug: debug)
            case 3: try checkinAvailable(debug: debug)
            case 4: try boarding(debug: debug)
            case 5: try landed(debug: debug)
            default:
                fatalError("No step '\(step)' defined")
            }
            print(jsonString)
        }
    }

    JSONPaload command line tool just parses input atttirbutes and executes its function associated:

    func boarding(debug: Bool) throws -> String {
        let contentState = FlightActivityAttributes.ContentState.boarding
        let push = PushPayload(
            aps: StartApsContent(
                contentState: contentState,
                attributesType: "FlightActivityAttributes",
                attributes: FlightActivityAttributes.bookingActivity
            )
        )
        let data = try JSONEncoder.pushDecoder(debug: debug).encode(push)
        return try data.prettyPrintedJSONString
    }

    FlightActivityAttributes.ContentState.boarding is same sample data code used also in the app (and widget). Is packaged into LiveActivityData because in that way allows data structure being used by command line tool. This is how PayloadGenerator/Package file declare its dependency with LiveActivityData package:

    import PackageDescription
    
    let package = Package(
        name: "PayloadGenerator",
        platforms: [.macOS(.v15)],
        dependencies: [
            .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
            .package(path: "../LiveActivityData"),
        ],
        targets: [
            // Targets are the basic building blocks of a package, defining a module or a test suite.
            // Targets can depend on other targets in this package and products from dependencies.
            .executableTarget(
                name: "PayloadGenerator",
                dependencies: [
                    .product(name: "ArgumentParser", package: "swift-argument-parser"),
                    .product(name: "LiveActivityData", package: "LiveActivityData"),
                ]
            ),
        ]
    )
    

    And this is how is set this dependency on iOS app:

    Add package dependency to project, and the import  dependency at time of coding

    ...
    import LiveActivityData
    
    // MARK: - View
    
    struct BookingBoardView: View {
        @StateObject private var controller = BookingBoardViewModel()
    ...

    After sending notification user should have to see in device and also in Apple Watch following:

    Landing

    Your Live Activity for a flight reservation isn’t the only one running on the iOS device. In the following screen sequence, you can see that while a screen recording Live Activity is active, a push notification arrives with updated flight landing information.

    When the push notification arrives, the Dynamic Island first presents it in expanded mode, but shortly after it switches to minimal mode (showing only the app icon). iOS itself decides the order and priority in which multiple Live Activities are presented.

    ...
    struct BookingFlightLiveActivity: Widget {
        var body: some WidgetConfiguration {
            ActivityConfiguration(for: FlightActivityAttributes.self) { context in
    ...
            } dynamicIsland: { context in
    ...
                return DynamicIsland {
                    ...
                } compactLeading: {
    ...
                } compactTrailing: {
    ...
                } minimal: {
                    MinimalView()
                }
            }
            .supplementalActivityFamilies([.small, .medium])
        }
    }

    When reviewing the Widget and Dynamic Island implementation, we can see that there is a section dedicated to defining the minimal view.

    Conclusions

    Implementing Live Activities in an iOS app enhances user experience by providing real-time, glanceable updates on the Lock Screen and Dynamic Island for ongoing, time-sensitive tasks like deliveries, rides, workouts,  live scores or your next flight information. Unlike notifications, which can clutter, Live Activities consolidate progress into a single, dynamic view, keeping users engaged without requiring repeated app opens. They complement widgets by handling short-lived, frequently changing processes while widgets cover persistent summaries. This leads to higher engagement, reduced notification fatigue, improved transparency, and stronger brand presence at high-attention moments—all while offering users quick actions and continuity across app surfaces.

    You can find source code that we have used for conducting this post in following GitHub repository.

    References

  • New Icon Composer with XCode 26

    New Icon Composer with XCode 26

    I’m going to walk you through how to use our new tool, Icon Composer—now integrated into the beta release of Xcode 26—to help you design app icons that feel right at home on iPhone, iPad, Mac, and Apple Watch.

    Tools Required

    Photopea (photopea.com) is a free, web-based alternative to Adobe Photoshop. I used it to design the initial layout of the icon.


    Icon Composer is Apple’s new tool for designers and developers, designed to simplify the process of building app icons for multiple platforms and appearance modes, including light and dark. You can download it from Apple Developer Resources.


    Xcode 26 (currently in beta) is the only version of Xcode that supports the new icon format. I used it to integrate the generated icon into a brand-new app project. You can download it from Apple Developer Resources.

    Icon design

    I’m planning to migrate the icon from my beloved Emom Timers app as soon as iOS 26 is released. Using Photopea, I’ve created a composition to visualize how the layers in the new icon will look.

    Screenshot

    The canvas is set to 1024 × 1024, and only the monochrome layered images are needed—colors and gradients will be handled later by Icon Composer. The next step is to export the layers as .png files. To do this, go to File (Archivo) > Export Layers (Exportar capas…).

    Icon Composer

    Once Icon Composer has been properly installed, just open it:

    Screenshot

    Then, drag the layered .png files previously generated with Photopea.

    Screenshot

    The interface is very intuitive. On the left panel, you’ll see the layers; in the center, the icon composition with controls that let you toggle layer guides and light effects at the top. At the bottom, you can preview how the icon will appear on iOS, macOS, or Apple Watch, including in light, dark, and monochrome modes. The right panel is dedicated to applying colors, blending modes, and the liquid crystal effect.

    Screenshot

    Once all the work is done, don’t forget to save the icon, as we’ll need it in the next step.

    Integrating on XCode 26

    Xcode 26 is the first IDE to support icons created with Icon Composer, so make sure it’s properly installed. Then, create a new iOS project and drag the previously generated Icon Composer file into your project’s source folder:

    Screenshot

    Icon Composer is also integrated into Xcode, so for future changes to the icon, you can open it directly from there. The icon is already included in the project source, but the target is not yet configured to use the new icon.

    Screenshot

    Now deploy the app to a device to see how the icon looks.

    Conclusions

    Icon Composer represents a significant step forward in creating and maintaining app icons. This tool allows designers (and developers) to easily preview how an icon will look across all platforms and appearance modes.

    You can find source code that we have used for conducting this post in following GitHub repository.

    References

  • Interaction and Media Accessibility in iOS

    Interaction and Media Accessibility in iOS

    Accessibility in iOS apps goes beyond just visual elements—it also includes interaction and media. In this post, we’ll focus on interaction and media-related accessibility topics, completing our coverage of accessibility in iOS 17.

    As we go through the post, I’ll share a sample project and source code to illustrate the key concepts.

    This post is a continuation of our previous article on visual accessibility in iOS.

    Voice Control

    Voice Control accessibility belongs to Ineraction group and is a feature that allows users to navigate and interact with their device entirely through spoken commands, without needing to touch the screen. It enables control over system-wide functions and app interfaces, such as tapping buttons, swiping, typing, dictating text, and navigating menus using voice instructions. Designed for users with limited mobility, Voice Control works by recognizing labels, accessibility identifiers, and numbered overlays on screen elements, making it essential for developers to provide clear and descriptive accessibility metadata in their apps to ensure seamless and inclusive user experiences.

    We will start by conding following chunk:

    struct VoiceControlView: View {
        @State private var name: String = ""
        @State private var message: String = ""
    
        var body: some View {
            VStack(spacing: 20) {
                Text("Voice Control Example")
                    .font(.title)
                    .accessibilityAddTraits(.isHeader)
                
                TextField("Enter your name", text: $name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                    .accessibilityLabel("Name Field")
                    .accessibilityIdentifier("nameField")
    
                Button(action: {
                    message = "Hello, \(name)!"
                }) {
                    Text("Submit")
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .accessibilityLabel("Submit Button")
                .accessibilityIdentifier("submitButton")
    
                if !message.isEmpty {
                    Text(message)
                        .font(.headline)
                        .accessibilityLabel(message)
                }
    
                Spacer()
            }
            .padding()
        }
    }

    This SwiftUI code defines a simple view called VoiceControlView that demonstrates how to make an interface accessible using iOS Voice Control. It includes a title, a text field for entering a name, and a submit button that, when tapped (or activated by voice), displays a personalized greeting message. Accessibility labels and identifiers are provided for the text field and button, allowing users to interact with them using voice commands like «Tap Name Field» or «Tap Submit Button.» The view is designed to be fully accessible, enhancing usability for users who rely on voice input for navigation and interaction.

    For a more realistic experience, I recommend running the demo on a real device. Note that Voice Control is not enabled by default—you’ll need to turn it on by going to Settings > Accessibility > Voice Control.

    Screenshot
    Screenshot 2025-07-26 at 20.43.11

    Deploy the app in a real device, and if Voice Control was setup properly, you should have to see following:

    Screenshot 2025-07-26 at 20.57.13
    Screenshot 2025-07-26 at 20.56.05

    Instead of tapping controls on the device screen, simply say «Tap» followed by the label shown on the control you want to interact with. For example, say «Tap More» to access the view dedicated to Voice Control, «Tap Name» to start entering text into the input field, and «Tap Submit» to submit the form.

    Voice Over

    VoiceOver belongs to Ineraction group and is an iOS accessibility feature that provides spoken feedback to help users with visual impairments navigate and interact with their devices. It reads aloud on-screen elements such as buttons, labels, text, and images, and allows users to control their device using gestures tailored for non-visual interaction. When VoiceOver is enabled, users can explore the interface by dragging a finger across the screen or using swipe gestures to move between elements, with each item being read aloud. Developers can enhance VoiceOver support by providing descriptive accessibility labels, traits, and hints to ensure that all content is understandable and usable without relying on sight.

    Lets take a look at the code  prepared for voice over:

    struct VoiceOverView: View {
        @State private var isToggled = false
           
           var body: some View {
               VStack(spacing: 40) {
                   Text("VoiceOver Interaction Example")
                       .font(.title)
                       .accessibilityAddTraits(.isHeader)
                   
                   Button(action: {
                       isToggled.toggle()
                   }) {
                       Image(systemName: isToggled ? "checkmark.circle.fill" : "circle")
                           .resizable()
                           .frame(width: 60, height: 60)
                           .foregroundColor(isToggled ? .green : .gray)
                   }
                   .accessibilityLabel("Toggle button")
                   .accessibilityValue(isToggled ? "On" : "Off")
                   .accessibilityHint("Double tap to change the toggle state")
    
                   Text("Toggle is currently \(isToggled ? "On" : "Off")")
               }
               .padding()
           }
    }

    The VoiceOverView SwiftUI struct defines a user interface that includes a header, a toggle button represented by an icon, and a status text label, with full VoiceOver accessibility support. The toggle button switches between a green checkmark and a gray circle when tapped, reflecting its «On» or «Off» state. For VoiceOver users, the button is described as «Toggle button» with a dynamic accessibility value («On» or «Off») and a hint explaining that a double-tap changes its state. The header is marked with .isHeader to assist navigation, and the toggle state is also shown in plain text. This setup ensures visually impaired users can fully interact with and understand the UI using VoiceOver.

    Deploy in the simulator and present the screen to audit and open Accesibility Inspector, select simulator and Run Audit:

    Captions

    Captions belong to the Media Accessibility group and consist of providing text descriptions of dialogue and other audible content during media playback. They help people who are deaf or hard of hearing access important information such as speech, nonverbal communication, music, and sound effects. Ensure that users can enable captions for all video or audio content. In this case, there is nothing to code—just make sure that if your app plays video, the content includes the option to enable captions (speech, non-verbal communication, music or sound effects).

    Audio descriptions

    Audio Descriptions belong to the Media Accessibility group and provide spoken narration during natural pauses in the main audio to describe important visual elements of the content. This helps people who are blind or have low vision understand what is happening on screen. Make sure users can easily find content that includes audio descriptions. For example, most video streaming apps display an «AD» icon to indicate availability. As with captions, this accessibility requirement applies to the content being played—not the app itself.

    Conclusions

    With this post, we wrap up the remaining accessibility groups—Interaction and Media—that we set aside in a previous post. The Interaction group includes coding tasks that require implementation, while the Media group mainly concerns the accessibility of streamed content within the app, so there’s typically nothing to code.

    You can find source code that we have used for conducting this post in following GitHub repository.

    References