Etiqueta: SwiftUI

  • 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

  • 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

  • Visual Accessibilty in iOS

    Visual Accessibilty in iOS

    Accessibility in iOS apps is a powerful way to highlight the importance of inclusive design while showcasing the robust accessibility tools Apple provides, such as VoiceOver, Dynamic Type, and Switch Control. It not only helps fellow developers understand how to create apps that are usable by everyone, including people with disabilities, but also demonstrates professionalism, empathy, and technical depth. By promoting best practices and raising awareness.

    For this post, we will focus only on visual accessibility aspects. Interaction and media-related topics will be covered in a future post. As we go through this post, I will also provide the project along with the source code used to explain the concepts.

    Accessibility Nutrition Labels

    Accessibility Nutrition Labels in iOS are a developer-driven concept inspired by food nutrition labels, designed to provide a clear, standardized summary of an app’s accessibility features. They help users quickly understand which accessibility tools—such as VoiceOver, Dynamic Type, or Switch Control—are supported, partially supported, or missing, making it easier for individuals with disabilities to choose apps that meet their needs. Though not a native iOS feature, these labels are often included in app. Eventhought accessibility is supported in almost all Apple platforms, some accessibility labels aren’t available check here.

    They can be set at the moment that you upload a new app version on Apple Store.

    Sufficient contrast

    Users can increase or adjust the contrast between text or icons and the background to improve readability. Adequate contrast benefits users with reduced vision due to a disability or temporary condition (e.g., glare from bright sunlight). You can indicate that your app supports “Sufficient Contrast” if its user interface for performing common tasks—including text, buttons, and other controls—meets general contrast guidelines (typically, most text elements should have a contrast ratio of at least 4.5:1). If your app does not meet this minimum contrast ratio by default, it should offer users the ability to customize it according to their needs, either by enabling a high-contrast mode or by applying your own high-contrast color palettes. If your app supports dark mode, be sure to check that the minimum contrast ratio is met in both light and dark modes.

     We have prepared following  following screen, that clearly does not follow this nutrition label:

    Deploy in the simulator and present the screen to audit and open Accesibility Inspector:

    Screenshot

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

    Important point, only are audited visible view layers:

    Buid and run on a simulator for cheking that all is working fine:

    Dark mode

    Dark Mode in SwiftUI is a user interface style that uses a darker color palette to reduce eye strain and improve visibility in low-light environments. SwiftUI automatically supports Dark Mode by adapting system colors like .primary, .background, and .label based on the user’s system settings. You can customize your UI to respond to Dark Mode using the @Environment(\.colorScheme) property.

    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 08.43.32
    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 08.43.41

    We’ve designed our app to support Dark Mode, but to ensure full compatibility, we’ll walk through some common tasks. We’ll also test the app with Smart Invert enabled—an accessibility feature that reverses interface colors.

    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 12.24.42
    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 12.25.15

    Once smart invert is activated all colors are set in their oposite color. This is something that we to have to avoid in some components of our app such as images.

                        AsyncImage(url: URL(string: "https://www.barcelo.com/guia-turismo/wp-content/uploads/2022/10/yakarta-monte-bromo-pal.jpg")) { image in
                             image
                                 .resizable()
                                 .scaledToFit()
                                 .frame(width: 300, height: 200)
                                 .cornerRadius(12)
                                 .shadow(radius: 10)
                         } placeholder: {
                             ProgressView()
                                 .frame(width: 300, height: 200)
                         }
                         .accessibilityIgnoresInvertColors(isDarkMode)

    In case of images or videos we have to avoid color inversion, we can make this happen by adding accessibilityIgnoreInvertColors modifier.

    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 08.43.32
    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 08.43.41

    It’s important to verify that media elements, like images and videos, aren’t unintentionally inverted. Once we’ve confirmed that our app maintains a predominantly dark background, we can confidently include Dark Interface in our Accessibility Nutrition Labels.

    Larger Text

    In Swift, «Larger Text» under Accessibility refers to iOS’s Dynamic Type system, which allows users to increase text size across apps for better readability. When building a nutrition label UI, developers should support these settings by using Dynamic Type-compatible fonts (like .body, .title, etc.), enabling automatic font scaling (adjustsFontForContentSizeCategory in UIKit or .dynamicTypeSize(...) in SwiftUI), and ensuring layouts adapt properly to larger sizes. This ensures the nutrition label remains readable and accessible to users with visual impairments, complying with best practices for inclusive app design.

    You can increase dynamic type in simulator in two ways, first one is by using accesibilty inspector, second one is by opening device Settings, Accessibilty, Display & Text Size, Larger text:

    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 13.00.31

    When se set the text to largest size we observe following:

    simulator_screenshot_F5937212-D584-449C-AE40-BFE3BEE486A3

    Only navigation title is being resized the rest of the content keeps the same size, this is not so much accessible!.

    When we execute accesibilty inspectoron this screen also complains.

    By replacing fixed font size by the type of font (.largeTitle, .title, .title2, .title3, .headline, .subheadline, .body, .callout, .footnote and .caption ). Remve also any frame fixed size that could cut any contained text. 

     func contentAccessible() -> some View {
            VStack(alignment: .leading, spacing: 8) {
                Text("Nutrition Facts")
                    .font(.title)
                    .bold()
                    .accessibilityAddTraits(.isHeader)
    
                Divider()
    
                HStack {
                    Text("Calories")
                        .font(.body)
                    Spacer()
                    Text("200")
                        .font(.body)
                }
    
                HStack {
                    Text("Total Fat")
                        .font(.body)
                    Spacer()
                    Text("8g")
                        .font(.body)
                }
    
                HStack {
                    Text("Sodium")
                        .font(.body)
                    Spacer()
                    Text("150mg")
                        .font(.body)
                }
            }
            .padding()
            .navigationTitle("Larger Text")
        }
    simulator_screenshot_80105F6D-0B6A-4899-ACEF-4CE44221E330

    We can observe how the view behaves when Dynamic Type is adjusted from the minimum to the maximum size. Notice that when the text «Nutrition Facts» no longer fits horizontally, it wraps onto two lines. The device is limited in horizontal space, but never vertically, as vertical overflow is handled by implementing a scroll view.

    Differentiate without color alone

    Let’s discuss color in design. It’s important to remember that not everyone perceives color the same way. Many apps rely on color—like red for errors or green for success—to convey status or meaning. However, users with color blindness might not be able to distinguish these cues. To ensure accessibility, always pair color with additional elements such as icons or text to communicate important information clearly to all users.

    Accessibility inspector, and also any color blinded person, will complain. For fixing use any shape or icon, apart from the color.l

    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 16.59.40
    Simulator Screenshot - iPhone 16 Pro - 2025-07-19 at 16.59.46

    Reduced motion

    Motion can enhance the user experience of an app. However, certain types of motion—such as zooming, rotating, or peripheral movement—can cause dizziness or nausea for people with vestibular sensitivity. If your app includes these kinds of motion effects, make them optional or provide alternative animations.

    Lets review the code:

    struct ReducedMotionView: View {
        @Environment(\.accessibilityReduceMotion) var reduceMotion
        @State private var spin = false
        @State private var scale: CGFloat = 1.0
        @State private var moveOffset: CGFloat = -200
    
        var body: some View {
            NavigationView {
                ZStack {
                    // Background rotating spiral
                    Circle()
                        .strokeBorder(Color.purple, lineWidth: 10)
                        .frame(width: 300, height: 300)
                        .rotationEffect(.degrees(spin ? 360 : 0))
                        .animation(reduceMotion ? nil : .linear(duration: 3).repeatForever(autoreverses: false), value: spin)
    
                    // Scaling and bouncing circle
                    Circle()
                        .fill(Color.orange)
                        .frame(width: 100, height: 100)
                        .scaleEffect(scale)
                        .offset(x: reduceMotion ? 0 : moveOffset)
                        .animation(reduceMotion ? nil : .easeInOut(duration: 1).repeatForever(autoreverses: true), value: scale)
                }
                .onAppear {
                    if !reduceMotion {
                        spin = true
                        scale = 1.5
                        moveOffset = 200
                    }
                }
                .padding()
                .overlay(
                    Text(reduceMotion ? "Reduce Motion Enabled" : "Extreme Motion Enabled")
                        .font(.headline)
                        .padding()
                        .background(Color.black.opacity(0.7))
                        .foregroundColor(.white)
                        .cornerRadius(12)
                        .padding(),
                    alignment: .top
                )
            }
            .navigationTitle("Reduced motion")
        }
    }

    The ReducedMotionView SwiftUI view creates a visual demonstration of motion effects that adapts based on the user’s accessibility setting for reduced motion. It displays a rotating purple spiral in the background and an orange circle in the foreground that scales and moves horizontally. When the user has Reduce Motion disabled, the spiral continuously rotates and the orange circle animates back and forth while scaling; when Reduce Motion is enabled, all animations are disabled and the shapes remain static. A label at the top dynamically indicates whether motion effects are enabled or reduced, providing a clear visual contrast for accessibility testing.

    Reduce Motion accessibility is not about removing animations from your app, but disable them when user has disabled Reduce Motion device setting.

     

    Pending

    Yes, this post is not complete yet. There are two families of Nutrition accessibility labels: Interaction and Media. I will cover them in a future post.

    Conclusions

    Apart from the benefits that accessibility provides to a significant group of people, let’s not forget that disabilities are diverse, and as we grow older, sooner or later we will likely need to use accessibility features ourselves. Even people without disabilities may, at some point, need to focus on information under challenging conditions—like poor weather—which can make interaction more difficult. It’s clear that this will affect us as iOS developers in how we design and implement user interfaces in our apps.

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

    References

  • Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Implementing Sign in with Apple in a client-server setup is valuable because it addresses a real-world need that many developers face, especially as Apple requires apps offering third-party login to support it. While Apple’s documentation focuses mainly on the iOS side, there’s often a gap in clear explanations for securely validating Apple ID tokens on the backend — a critical step to prevent security vulnerabilities.

    Since Node.js is a widely used backend for mobile apps, providing a practical, end-to-end guide would help a large audience, fill a common knowledge gap, and position you as an expert who understands both mobile and server-side development, making the post highly useful, shareable, and relevant.

    Apple Sign in

    Apple Sign In is a secure authentication service introduced by Apple in 2019, allowing users to log in to apps and websites using their Apple ID. It emphasizes privacy by minimizing data sharing with third parties, offering features like hiding the user’s real email address through a unique, auto-generated proxy email. Available on iOS, macOS, and web platforms, it provides a fast and convenient alternative to traditional social logins like Google or Facebook.

    Advantages of Apple Sign In
    One of the biggest advantages is enhanced privacy—Apple does not track user activity across apps, and the «Hide My Email» feature protects users from spam and data leaks. It also simplifies the login process with Face ID, Touch ID, or device passcodes, reducing password fatigue. Additionally, Apple Sign In is mandatory for apps that offer third-party logins on iOS, ensuring wider adoption and consistent security standards.

    Inconveniences of Apple Sign In
    A major drawback is its limited availability, as it only works on Apple devices, excluding Android and Windows users. Some developers also criticize Apple for forcing its use on iOS apps while restricting competitor login options. Additionally, if a user loses access to their Apple ID, account recovery can be difficult, potentially locking them out of linked services. Despite these issues, Apple Sign In remains a strong choice for privacy-focused users.

    Dockerized Node.JS server side

    Start by setting up a blank Node.js server using Express.js to handle HTTP requests.

    npm init -y

    Server.js code is following:

    const express = require('express');
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware for parsing JSON
    app.use(express.json());
    
    // Client for look up public keys at Apple
    const client = jwksClient({
        jwksUri: 'https://appleid.apple.com/auth/keys'
    });
    
    // Function for getting public key
    function getAppleKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
            if (err) {
                callback(err);
            } else {
                const signingKey = key.getPublicKey();
                callback(null, signingKey);
            }
        });
    }
    
    // Route for authenticate
    app.post('/auth/apple', (req, res) => {
        const { identityToken } = req.body;
    
        if (!identityToken) {
            return res.status(400).json({ error: 'identityToken missing' });
        }
    
        jwt.verify(identityToken, getAppleKey, {
            algorithms: ['RS256']
        }, (err, decoded) => {
            if (err) {
                console.error('Error verifying token:', err);
                return res.status(401).json({ error: 'Invalid token' });
            }
    
            // decoded contains user data
            console.log('Token verified:', decoded);
    
            res.json({
                success: true,
                user: {
                    id: decoded.sub,
                    email: decoded.email,
                    email_verified: decoded.email_verified
                }
            });
        });
    });
    
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
    });
    

    server.js sets up an Express server that listens for authentication requests using Apple’s Sign-In service. It imports necessary modules like express for routing, jsonwebtoken for verifying JSON Web Tokens (JWTs), and jwks-rsa for retrieving Apple’s public keys used to validate tokens. The server is configured to parse incoming JSON payloads and uses environment variables (loaded via dotenv) to optionally define a custom port.

    The core logic resides in the /auth/apple POST route. When a client sends a request to this endpoint with an identityToken in the body (typically issued by Apple after a successful login), the server first checks if the token is present. It then verifies the token using jsonwebtoken.verify(), passing a custom key retrieval function (getAppleKey). This function uses the jwksClient to fetch the appropriate public key from Apple’s JWKS (JSON Web Key Set) endpoint based on the kid (Key ID) found in the token header.

    If the token is valid, the decoded payload—which includes user-specific data like sub (user ID), email, and email_verified—is extracted and returned in the response as JSON. If token verification fails, an error response with HTTP 401 status is sent. This setup allows backend applications to securely validate Apple identity tokens without hardcoding public keys, keeping the authentication mechanism both dynamic and secure.

    Server is dockerized:

    FROM node:20
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    This Dockerfile sets up a Node.js environment using the node:20 base image, creates a working directory at /usr/src/app, copies package.json and package-lock.json (if present) into it, installs dependencies with npm install, copies the rest of the application files, exposes port 3000 for the container, and finally runs the npm start command to launch the application.

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

    docker run -p 3000:3000 apple-signin-server

    Server ready for receiving requests…

    Client iOS Apple Sign in app

    After creating a simple iOS app project, go to the target settings and add the ‘Sign in with Apple’ capability. Then, start by creating a blank Node.js server.

    The next step is the client code itself:

    import SwiftUI
    import AuthenticationServices
    
    struct ContentView: View {
        @State private var userID: String?
        @State private var userEmail: String?
        @State private var userName: String?
        
        var body: some View {
            VStack(spacing: 20) {
                if let userID = userID {
                    Text("Welcome 🎉")
                        .font(.title)
                    Text("User ID: \(userID)")
                    if let name = userName {
                        Text("Name: \(name)")
                    }
                    if let email = userEmail {
                        Text("Email: \(email)")
                    }
                } else {
                    SignInWithAppleButton(
                        .signIn,
                        onRequest: { request in
                            request.requestedScopes = [.fullName, .email]
                        },
                        onCompletion: { result in
                            switch result {
                            case .success(let authorization):
                                handleAuthorization(authorization)
                            case .failure(let error):
                                print("Authentication error: \(error.localizedDescription)")
                            }
                        }
                    )
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 280, height: 50)
                    .cornerRadius(8)
                    .padding()
                }
            }
            .padding()
        }
        
        private func handleAuthorization(_ authorization: ASAuthorization) {
            if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                userID = appleIDCredential.user
                userEmail = appleIDCredential.email
                if let fullName = appleIDCredential.fullName {
                    userName = [fullName.givenName, fullName.familyName]
                        .compactMap { $0 }
                        .joined(separator: " ")
                }
                
                if let identityToken = appleIDCredential.identityToken,
                   let tokenString = String(data: identityToken, encoding: .utf8) {
                    authenticateWithServer(identityToken: tokenString)
                }
            }
        }
        
        private func authenticateWithServer(identityToken: String) {
            guard let url = URL(string: "http://localhost:3000/auth/apple") else { return }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = ["identityToken": identityToken]
            
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data,
                   let json = try? JSONSerialization.jsonObject(with: data) {
                    print("Server response:", json)
                } else {
                    print("Error communicating with server:", error?.localizedDescription ?? "Unknown error")
                }
            }.resume()
        }
    }
    
    
    #Preview {
        ContentView()
    }
    

    It defines a user interface for an iOS app that integrates Sign in with Apple. The core logic is built into the ContentView struct, which maintains state variables to store the signed-in user’s ID, name, and email. When the view is rendered, it checks whether the user is already signed in (i.e., if userID is not nil). If the user is authenticated, it displays a welcome message along with the retrieved user details. If not, it shows a «Sign in with Apple» button that initiates the authentication process when tapped.

    When the «Sign in with Apple» button is pressed, it triggers a request for the user’s full name and email. The result of this action is handled in the onCompletion closure. If the sign-in is successful, the handleAuthorization method is called. This function extracts the user’s credentials from the ASAuthorizationAppleIDCredential object, including their user ID, email, and full name (if provided). It also extracts the identity token (a JSON Web Token), which is used to authenticate the user on the app’s backend server.

    The authenticateWithServer function handles the server-side communication. It sends a POST request to http://localhost:3000/auth/apple, passing the identityToken in the JSON body. This token can be verified on the backend to ensure the identity is legitimate and secure. The response from the server (or any error encountered) is printed to the console. This architecture supports secure, privacy-preserving user login using Apple’s authentication services, commonly used in modern iOS apps.

    Apple Sign in integration

    Deploy iOS app with Apple Sign-In in a simulator (not on a real device).

    review

    Simply sign in using your personal iCloud credentials. Once Apple Sign-In is successful on the client side, it sends a request and provides the identityToken.

    Even if you uninstall the app from the device, the identityToken remains unchanged. Therefore, it can reliably be used as a user identifier.

    Conclusions

    From a programming perspective, implementing Apple Sign-In in your apps is straightforward and enhances privacy, as users can choose whether to share their email.

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

    References

  • The MVVM-C Blueprint for iOS Apps

    The MVVM-C Blueprint for iOS Apps

    The MVVM-C pattern, which combines the Model-View-ViewModel (MVVM) architecture with a Coordinator layer, offers a structured approach to building scalable and maintainable iOS apps. It effectively separates concerns, making the codebase more modular and easier to test.

    In this tutorial, we will implement a sample focused solely on its navigation components. At the end of the post, you will find the GitHub repository where you can access the sample project used for this tutorial.

    The coordinator component

    In the MVVM-C (Model-View-ViewModel-Coordinator) pattern, the Coordinator is responsible for managing navigation and application flow, ensuring that the View and ViewModel remain focused on UI presentation and business logic, respectively, without being concerned with navigation and flow management. It handles the creation and configuration of View and ViewModel instances, determines which screen to display next based on user actions or app logic, and manages transitions between screens. By centralizing navigation logic, the Coordinator promotes modularity, reusability, and testability, maintaining a clean and scalable architecture.

    Depending on the complexity of the app, the Coordinator can be implemented in different ways:

    • Whole App Coordinator – Best for small apps with a few screens, where a single component can effectively manage the navigation flow.
    • Flow Coordinator – In larger apps, a single coordinator becomes difficult to manage. Grouping screens by business flow improves modularity and maintainability.
    • Screen Coordinator – Each screen has its own dedicated coordinator, making it useful for reusable components, such as a payment screen appearing in different user journeys. This approach is often used in architectures like VIPER, where each module operates independently.

    Ultimately, the choice of implementation depends on the app’s complexity and business requirements; no single pattern fits all use cases.

    The sample app

    The app we are going to implement is a Tab View app. Each tab represents a different navigation flow:

    flowtab1
    Screenshot

    The First Tab Flow is a flow coordinator that presents a Primary View with two buttons. These buttons navigate to either Secondary View 1 or Secondary View 2.

    • When Secondary View 2 appears, it includes a button that allows navigation to Tertiary View 1.
    • In Tertiary View 1, there is a button that returns directly to the Primary View or allows navigation back to the previous screen using the back button.
    • Secondary View 2 does not lead to any additional views; users can only return to the previous screen using the back button.

    The Second Tab Flow is managed by a Screen Coordinator, which presents a single screen with a button that opens a view model.

    • In this context, we consider the modal to be part of the view.
    • However, depending on the app’s design, the modal might instead be managed by the coordinator.

    Main Tab View

    This is the entry view point from the app:

    struct MainView: View {
        @StateObject private var tab1Coordinator = Tab1Coordinator()
        @StateObject private var tab2Coordinator = Tab2Coordinator()
    
        var body: some View {
            TabView {
                NavigationStack(path: $tab1Coordinator.path) {
                    tab1Coordinator.build(page: .primary)
                        .navigationDestination(for: Tab1Page.self) { page in
                            tab1Coordinator.build(page: page)
                        }
                }
                .tabItem {
                    Label("Tab 1", systemImage: "1.circle")
                }
    
                NavigationStack(path: $tab2Coordinator.path) {
                    tab2Coordinator.build(page: .primary)
                        .navigationDestination(for: Tab2Page.self) { page in
                            tab2Coordinator.build(page: page)
                        }
                }
                .tabItem {
                    Label("Tab 2", systemImage: "2.circle")
                }
            }
        }
    }

    The provided SwiftUI code defines a MainView with a TabView containing two tabs, each managed by its own coordinator (Tab1Coordinator and Tab2Coordinator). Each tab uses a NavigationStack bound to the coordinator’s path property to handle navigation. The coordinator’s build(page:) method constructs the appropriate views for both the root (.primary) and subsequent pages.

    The navigationDestination(for:) modifier ensures dynamic view creation based on the navigation stack, while the tabItem modifier sets the label and icon for each tab. This structure effectively decouples navigation logic from the view hierarchy, promoting modularity and ease of maintenance.

    Another key aspect is selecting an appropriate folder structure. The one I have chosen is as follows:

    Screenshot

    This may not be the best method, but it follows a protocol to prevent getting lost when searching for files.

    The flow coordinator

    The first structure we need to create is an enum that defines the screens included in the flow:

    enum Tab1Page: Hashable {
        case primary
        case secondary1
        case secondary2
        case tertiary
    }

    Hashable is not free; we need to push and pop those cases into a NavigationPath. The body of the coordinator is as follows:

    class Tab1Coordinator: ObservableObject {
        @Published var path = NavigationPath()
    
        func push(_ page: Tab1Page) {
            path.append(page)
        }
    
        func pop() {
            path.removeLast()
        }
    
        func popToRoot() {
            path.removeLast(path.count)
        }
    
        @ViewBuilder
           func build(page: Tab1Page) -> some View {
               switch page {
               case .primary:
                   Tab1PrimaryView(coordinator: self)
               case .secondary1:
                   Tab1SecondaryView1(coordinator: self)
               case .secondary2:
                   Tab1SecondaryView2()
               case .tertiary:
                   Tab1TertiaryView(coordinator: self)
               }
           }
    }

    The Tab1Coordinator class is an ObservableObject that manages navigation within a SwiftUI view hierarchy for a specific tab (Tab1). It uses a NavigationPath to track the navigation stack, allowing views to be pushed onto or popped from the stack through methods such as push(_:), pop(), and popToRoot(). The @Published property path ensures that any changes to the navigation stack are automatically reflected in the UI.

    The build(page:) method, marked with @ViewBuilder, constructs and returns the appropriate SwiftUI view (e.g., Tab1PrimaryView, Tab1SecondaryView1, Tab1SecondaryView2, or Tab1TertiaryView) based on the provided Tab1Page enum case. This approach enables dynamic navigation between views while maintaining a clean separation of concerns.

    The last section of the coordinator is the protocol implementation for the views presented by the coordinator. When a view has completed its work, it delegates the decision of which screen to present next to the coordinator. The coordinator is responsible for managing the navigation logic, not the view.

    extension Tab1Coordinator: Tab1PrimaryViewProtocol {
        func goToSecondary1() {
            push(.secondary1)
        }
        func goToSecondary2() {
            push(.secondary2)
        }
    }
    
    extension Tab1Coordinator: Tab1SecondaryView1Protocol {
        func goToTertiaryView() {
            push(.tertiary)
        }
    }
    
    extension Tab1Coordinator: Tab1TertiaryViewProtocol {
        func backToRoot() {
            self.popToRoot()
        }
    }
    

    This is the code from one of the views:

    import SwiftUI
    
    protocol Tab1PrimaryViewProtocol: AnyObject {
        func goToSecondary1()
        func goToSecondary2()
    }
    
    struct Tab1PrimaryView: View {
         let coordinator: Tab1PrimaryViewProtocol
        
            var body: some View {
                
                VStack {
                    Button("Go to Secondary 1") {
                        coordinator.goToSecondary1()
                    }
                    .padding()
    
                    Button("Go to Secondary 2") {
                        coordinator.goToSecondary2()
                    }
                    .padding()
                }
                .navigationTitle("Primary View")
            }
    }

    When the view doesn’t know how to proceed, it should call its delegate (the Coordinator) to continue.

    The screen coordinator

    The first structure we need to create is an enum that defines the screens in the flow:

    enum Tab2Page: Hashable {
        case primary
    }
    
    class Tab2Coordinator: ObservableObject {
        @Published var path = NavigationPath()
        
        @ViewBuilder
        func build(page: Tab2Page) -> some View {
            switch page {
            case .primary:
                Tab2PrimaryView(coordinator: self)
            }
        }
    }

    Hashable is not free; we need to push/pop these cases into a NavigationPath. The body of the coordinator is simply as follows:

    class Tab1Coordinator: ObservableObject {
        @Published var path = NavigationPath()
    
        func push(_ page: Tab1Page) {
            path.append(page)
        }
    
        func pop() {
            path.removeLast()
        }
    
        func popToRoot() {
            path.removeLast(path.count)
        }
    
        @ViewBuilder
           func build(page: Tab1Page) -> some View {
               switch page {
               case .primary:
                   Tab1PrimaryView(coordinator: self)
               case .secondary1:
                   Tab1SecondaryView1(coordinator: self)
               case .secondary2:
                   Tab1SecondaryView2()
               case .tertiary:
                   Tab1TertiaryView(coordinator: self)
               }
           }
    }

    The provided code defines a SwiftUI-based navigation structure for a tabbed interface. The Tab2Coordinator class is an ObservableObject that manages the navigation state using a NavigationPath, which is a state container for navigation in SwiftUI. The @Published property path allows the view to observe and react to changes in the navigation stack. The build(page:) method is a ViewBuilder that constructs the appropriate view based on the Tab2Page enum case. Specifically, when the page is .primary, it creates and returns a Tab2PrimaryView, passing the coordinator itself as a dependency.

    This approach is commonly used in SwiftUI apps to handle navigation between different views within a tab, promoting a clean separation of concerns and state management. The Tab2Page enum is marked as Hashable, which is required for it to work with NavigationPath.

    Conclusions

    Coordinator is a key component that allows to unload ViewModel or ViewModel logic for controlling navigation logic. I hope this post will help you to understand better this pattern.

    You can find the source code used for this post in the repository linked below.

    References

  • Dynamic Forms in SwiftUI for variable section type

    Dynamic Forms in SwiftUI for variable section type

    When programming a form in SwiftUI, the typical case involves forms with a fixed number of fields. These are forms like the ones you use when registering on a website. However, this is not the only type of form you might encounter. Sometimes, you may need to create forms that collect data for multiple entities, and these entities might not always be of the same type. For example, consider forms for booking a train or flight ticket, where different sections might be required for passengers, payment, and additional services.

    The approach to implementing dynamic, variable-section forms is quite different, as it involves working with Dynamic Bindings. In this post, you’ll learn how to handle this complexity effectively. By the end of the post, you’ll find a link to a GitHub repository containing the base code for this project.

    Dynamic sample SwiftUI app

    The sample app follows the MVVM architecture and implements a form for managing multiple persons. Each person is represented as a separate section in the form, and they can either be an Adult or a Child. Adults have fields for name, surname, and email, while Children have fields for name, surname, and birthdate. Validation rules are implemented, such as ensuring that a child’s age is under 18 years and that email addresses follow the correct syntax.

    We are going to create a person form for 2 adults and 1 child:
    struct ContentView: View {
        @StateObject private var viewModel = DynamicFormViewModel(persons: [
            .adult(Adult(name: "Juan", surename: "Pérez", email: "juan.perez@example.com")),
            .child(Child(name: "Carlos", surename: "Gomez", birthdate: Date(timeIntervalSince1970: 1452596356))),
            .adult(Adult(name: "Ana", surename: "Lopez", email: "ana.lopez@example.com"))
        ])
        
        var body: some View {
            DynamicFormView(viewModel: viewModel)
        }
    }
    At this point in view model we start to see different things
    class DynamicFormViewModel: ObservableObject {
        @Published var persons: [SectionType]
    ...
        init(persons: [SectionType]) {
            self.persons = persons
        }
    ...
    }
    Instead of having one @published attribute per field we have have an array of SectionType. 
    struct Adult: Identifiable {
        var id = UUID()
        var name: String
        var surename: String
        var email: String
    }
    
    struct Child: Identifiable {
        var id = UUID()
        var name: String
        var surename: String
        var birthdate: Date
    }
    
    enum SectionType {
        case adult(Adult)
        case child(Child)
    }

    SectionType is an enum (struct)  that could be Adult or a Child. Our job in the View now will be to create a new binding to attach to the current form field that is being rendered:

    struct DynamicFormView: View {
        @StateObject var viewModel: DynamicFormViewModel
    
        var body: some View {
            Form {
                ForEach(Array(viewModel.persons.enumerated()), id: \.offset) { index, persona in
                    Section {
                        if let adultoBinding = adultBinding(for: index) {
                            AdultForm(adulto: adultoBinding)
                                .environmentObject(viewModel)
                        }
                        if let niñoBinding = childBinding(for: index) {
                            ChildForm(niño: niñoBinding)
                                .environmentObject(viewModel)
                        }
                    }
                }
            }
        }
    
        private func adultBinding(for index: Int) -> Binding<Adult>? {
            guard case .adult(let adult) = viewModel.persons[index] else { return nil }
            return Binding<Adult>(
                get: { adult },
                set: { newAdult in viewModel.persons[index] = .adult(newAdult) }
            )
        }
    
        private func childBinding(for index: Int) -> Binding<Child>? {
            guard case .child(let child) = viewModel.persons[index] else { return nil }
            return Binding<Child>(
                get: { child },
                set: { newChild in viewModel.persons[index] = .child(newChild) }
            )
        }
    }

    The DynamicFormView dynamically renders a SwiftUI form where each section corresponds to a person from a DynamicFormViewModel‘s persons array, which contains enums distinguishing adults and children. Using helper methods, it creates Binding objects to provide two-way bindings for either an AdultForm or ChildForm based on the person’s type. These forms allow editing of the Adult or Child data directly in the view model. By leveraging SwiftUI’s ForEach, conditional views, and @EnvironmentObject, the view efficiently handles heterogeneous collections and updates the UI in response to changes.

    struct DynamicFormView: View {
        @StateObject var viewModel: DynamicFormViewModel
    
        var body: some View {
            Form {
                ForEach(Array(viewModel.persons.enumerated()), id: \.offset) { index, persona in
                    Section {
                        if let adultoBinding = adultBinding(for: index) {
                            AdultForm(adulto: adultoBinding)
                                .environmentObject(viewModel)
                        }
                        if let niñoBinding = childBinding(for: index) {
                            ChildForm(niño: niñoBinding)
                                .environmentObject(viewModel)
                        }
                    }
                }
            }
        }
    
        private func adultBinding(for index: Int) -> Binding<Adult>? {
            guard case .adult(let adult) = viewModel.persons[index] else { return nil }
            return Binding<Adult>(
                get: { adult },
                set: { newAdult in viewModel.persons[index] = .adult(newAdult) }
            )
        }
    
        private func childBinding(for index: Int) -> Binding<Child>? {
            guard case .child(let child) = viewModel.persons[index] else { return nil }
            return Binding<Child>(
                get: { child },
                set: { newChild in viewModel.persons[index] = .child(newChild) }
            )
        }
    }

    Finally, the implementation of AdultSectionForm (and ChildSectionForm) is nothing special and not commonly encountered in standard SwiftUI form development.

    struct AdultSectionForm: View {
        @Binding var adulto: Adult
        @EnvironmentObject var viewModel: DynamicFormViewModel
        
        var body: some View {
            VStack(alignment: .leading) {
                TextField("Name", text: $adulto.name)
                    .onChange(of: adulto.name) { newValue, _ in
                        viewModel.validateName(adultoId: adulto.id, nombre: newValue)
                    }
                if let isValid = viewModel.validName[adulto.id], !isValid {
                    Text("Name cannot be empty.")
                        .foregroundColor(.red)
                }
                
                TextField("Surename", text: $adulto.surename)
                
                TextField("Email", text: $adulto.email)
                    .onChange(of: adulto.email) { newValue, _ in
                        viewModel.validateEmail(adultoId: adulto.id, email: newValue)
                    }
                if let isValido = viewModel.validEmail[adulto.id], !isValido {
                    Text("Not valid email")
                        .foregroundColor(.red)
                }
            }
        }
    }

    Conclusions

    Handling dynamic forms in SwiftUI is slightly different from what is typically explained in books or basic tutorials. While it isn’t overly complicated, it does require a clear understanding, especially when implementing a form with such characteristics.

    In this post, I have demonstrated a possible approach to implementing dynamic forms. You can find the source code used for this post in the repository linked below.

    References