Autor: admin

  • 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

  • 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

  • Maintain, Share, Repeat: iOS Component Distribution

    Maintain, Share, Repeat: iOS Component Distribution

    Distribute and easily maintain a component is valuable because it addresses a common challenge in scaling and collaborating on iOS projects. By sharing strategies for modularizing code, using Swift Package Manager, applying semantic versioning, and setting up proper documentation and CI/CD workflows, developers can create reusable, testable, and maintainable components that boost productivity across teams.

    Just as important as creating scalable components is defining a solid framework for maintaining and distributing them. In this post, we’ll focus on the infrastructure side of that process.

    The Alert Component

    First step is creating the package that will hold our component by typing following 3 commands:

    $ mkdir AlertComponent
    $ cd AlertComponent
    $ swift package init --type library
    Screenshot

    Once the package scaffold is created, implement the component. In this post, the focus is on distribution, not maintenance or component scalability — which are certainly important and will be covered in future posts.

    import SwiftUI
    
    @available(macOS 10.15, *)
    public struct AlertView: View {
        let title: String
        let message: String
        let dismissText: String
        let onDismiss: () -> Void
    
        public init(title: String, message: String, dismissText: String = "OK", onDismiss: @escaping () -> Void) {
            self.title = title
            self.message = message
            self.dismissText = dismissText
            self.onDismiss = onDismiss
        }
    
        public var body: some View {
            VStack(spacing: 20) {
                Text(title)
                    .font(.headline)
                    .padding(.top)
    
                Text(message)
                    .font(.body)
    
                Button(action: onDismiss) {
                    Text(dismissText)
                        .bold()
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            .padding()
            .background(Color(.white))
            .cornerRadius(16)
            .shadow(radius: 10)
            .padding()
        }
    }
    

    It is a simple alert component. In a future post, we will explore how to improve its maintainability, scalability, and documentation. Most importantly, we will upload the code to a GitHub repository.

    generated

    Consume component

    For consume it this component we are going to create a simple iOS project, but this time via Tuist. Tuist is a powerful yet often underutilized tool that can greatly simplify project setup, modularization, and CI workflows, you can find an introduction to this technology in this post. At the end is clearer to track configuration changes in project by code.

     

    $ tuist init
    Screenshot

    Next is configuring SPM alert component package on Tuist:

    $ tuist edit

    Go to Project.swif, In packages add package url and version, and also in targets add package dependecy:

    import ProjectDescription
    
    let project = Project(
        name: "AlertComponentConsumer",
        packages: [
            .package(url: "https://github.com/JaCaLla/AlertComponent.git", from: "0.0.1")
        ],
        targets: [
            .target(
                name: "AlertComponentConsumer",
                ...
                dependencies: [
                    .package(product: "AlertComponent")
                ]
            ),
            .target(
                name: "AlertComponentConsumerTests",
              ...
                dependencies: [.target(name: "AlertComponentConsumer")]
            ),
        ]
    )
    

    Run twist generate and build de project for chacking that all is ok.

    $ tuist generate
    Screenshot

    Observe that component has been included as Package dependency:

    Update ContentView.swift for start playing with AlertComponent.

    import SwiftUI
    import AlertComponent
    
    struct ContentView: View {
        @State private var showAlert = false
    
        var body: some View {
            ZStack {
                Button("Show alert") {
                    showAlert = true
                }
    
                if showAlert {
                    AlertView(
                        title: "Warning",
                        message: "This is a personalized alert view",
                        onDismiss: {
                            showAlert = false
                        }
                    )
                    .background(Color.black.opacity(0.4).ignoresSafeArea())
                }
            }
        }
    }

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

    You can find the AlertComponentConsumer iOS project in our GitHub repository.

    Component maintenance

    From now on, we have both the component deployment and the final user component. Component developers not only write the source code, but also need to create a bench test project to properly develop and validate the component. This bench test project is also very useful for final developers, as it serves as documentation on how to integrate the component into their iOS projects.

    This project will be called ‘AlertComponentDemo’ and will be placed in a sibling folder from Alert Componet. This is not casuality because we add component source files to this project as a reference, so comopent developer in the same XCode project will be able to update component and bench test source code.

    We will use also Tuist for generating this project:

    $ tuist init
    Screenshot

    Remember, it is mandatory to know where we place the project folder because we will include component source files as references. In my case, I decided to keep the folder in the same parent directory as the siblings.

    Edit project with Tuist for including component source code references….

    let project = Project(
        name: "AlertComponentBench",
        targets: [
            .target(
                name: "AlertComponentBench",
               ...
                sources: ["AlertComponentBench/Sources/**",
                          "../AlertComponent/Sources/**"],
                ...
    )

    Genertate Xcode project with Tuist

    $ tuist generate
    Screenshot

    For simplicity, we will place the same code in ContentView.swift that we previously used in Consumer to verify that everything is properly integrated and functioning.

    Now the component is easier to maintain because, within the same project, the developer can manage both the component code and the code for starting to work with the component.

    Screenshot

    But code changes keept separately in two different reposiories:

    Screenshot

    This ensures that the component repository contains only the code specific to the component. You can find the AlertComponentBench iOS project in our GitHub repository.

    Component Control Version

    Having a version control is crucial for managing changes, ensuring stability, and supporting safe, modular development. It allows developers to track the evolution of the component, prevent breaking changes through semantic versioning, and let consumers lock to specific versions that are known to work. This setup fosters reliable updates, easier debugging, streamlined collaboration, and consistent integration in both personal and team projects. Ultimately, version control transforms a simple UI component into a maintainable, scalable, and production-ready package.

    As component is placed on GitHub we will implement a GitHub Action that will be triggered every time a pull request merges into main branch

    name: Tag on PR Merge to Main
    
    on:
      pull_request:
        types: [closed]
        branches:
          - main
    
    jobs:
      tag:
        if: github.event.pull_request.merged == true
        runs-on: ubuntu-latest
    
        steps:
          - name: Checkout repo
            uses: actions/checkout@v4
            with:
              fetch-depth: 0  # Needed to fetch tags
    
          - name: Set up Git
            run: |
              git config user.name "GitHub Actions"
              git config user.email "actions@github.com"
    
          - name: Get latest tag
            id: get_tag
            run: |
              latest=$(git describe --tags `git rev-list --tags --max-count=1` 2>/dev/null || echo "v0.0.0")
              echo "Latest tag: $latest"
              echo "tag=$latest" >> $GITHUB_OUTPUT
    
          - name: Determine bump type from PR title
            id: bump
            run: |
              title="${{ github.event.pull_request.title }}"
              echo "PR Title: $title"
              first_word=$(echo "$title" | awk '{print toupper($1)}')
              case $first_word in
                MAJOR)
                  echo "bump=major" >> $GITHUB_OUTPUT
                  ;;
                MINOR)
                  echo "bump=minor" >> $GITHUB_OUTPUT
                  ;;
                *)
                  echo "bump=patch" >> $GITHUB_OUTPUT
                  ;;
              esac
    
          - name: Calculate next version
            id: next_tag
            run: |
              tag="${{ steps.get_tag.outputs.tag }}"
              version="${tag#v}"
              IFS='.' read -r major minor patch <<< "$version"
    
              bump="${{ steps.bump.outputs.bump }}"
              case $bump in
                major)
                  major=$((major + 1))
                  minor=0
                  patch=0
                  ;;
                minor)
                  minor=$((minor + 1))
                  patch=0
                  ;;
                patch)
                  patch=$((patch + 1))
                  ;;
              esac
    
              next_tag="v$major.$minor.$patch"
              echo "Next tag: $next_tag"
              echo "next_tag=$next_tag" >> $GITHUB_OUTPUT
    
          - name: Create and push new tag
            run: |
              git tag ${{ steps.next_tag.outputs.next_tag }}
              git push origin ${{ steps.next_tag.outputs.next_tag }}

    This GitHub Actions workflow automatically creates and pushes a new semantic version tag whenever a pull request is merged into the main branch. It triggers on PR closures targeting main, and only proceeds if the PR was actually merged. It fetches the latest Git tag (defaulting to v0.0.0 if none exist), determines the version bump type based on the first word of the PR title (MAJOR, MINOR, or defaults to patch), calculates the next version accordingly, and pushes the new tag back to the repository. This enables automated versioning tied directly to PR titles.

    Now we are going to introduce some changes in the component.

    Commit changes in a separate branch:

    Prepare pull request:

    Create pull request

    Once you merge it, GitHub action will be executed.

    Let’s pull changes on main branch:

    main branch commit from pr has been tagged!

    Now let’s going to consume new component version, Tuist edit alert component consumer project:

    $ tuist edit
    import ProjectDescription
    
    let project = Project(
        name: "AlertComponentConsumer",
        packages: [
            .package(url: "https://github.com/JaCaLla/AlertComponent.git", from: "0.2.0")
        ],
      ...
    )
    
    $ tuist generate

    Build and deploy on simulator

    Simulator Screenshot - iPhone 16 - 2025-07-13 at 14.00.21

    Documentation

    Last but not least, having good documentation is a great starting point to encourage adoption and ensure a smooth integration process. It’s not about writing a myriad of lines — simply providing a well-written README.md file in the GitHub repository with clear and basic information is enough to help developers start using the component effectively.

    Conclusions

    Developing a component is not enough; it also requires providing a reusable and scalable architecture, along with delivery mechanisms that enable fast distribution and the ability to track which exact version of the component consumers are using.

    References

  • Seamless Keychain Data Migrations in iOS

    Seamless Keychain Data Migrations in iOS

    It’s a common but poorly documented challenge that many developers face, especially during device upgrades, app reinstallations, or when sharing data across app groups. Since the Keychain stores sensitive user information like credentials and tokens, handling its migration securely is critical for maintaining a seamless user experience and ensuring data integrity.

    This post explains how to implement a trial period for users in an iOS app. To prevent users from restarting the trial period by reinstalling the app, the controlling information should be securely stored in the Keychain.

    Keychain

    Keychain in iOS is a secure storage system provided by Apple that allows apps to store sensitive information such as passwords, cryptographic keys, and certificates securely. It uses strong encryption and is protected by the device’s hardware and the user’s authentication (e.g., Face ID, Touch ID, or passcode). The Keychain ensures that this data is safely stored and only accessible to the app that created it, unless explicitly shared through keychain access groups. It provides a convenient and secure way for developers to manage credentials and other private data without implementing their own encryption systems.

    When an app is removed (uninstalled) from an iOS device, most of its data—including files stored in its sandboxed file system—is deleted. However, data stored in the Keychain is not automatically deleted when the app is removed. This means that if the app is reinstalled later, it can still access its previously stored Keychain data (assuming the Keychain item was saved with the correct accessibility settings and not tied to a now-invalid access group). This behavior allows for features like remembering a user’s login after reinstalling an app.

    Persisted local data can change over the lifetime of an application, and apps distributed in the past may not always be updated to the latest version. To handle this, apps must implement a data migration mechanism to adapt old stored data to the new data model. Without proper migration, the app may crash when attempting to load outdated or incompatible data. When data is stored in UserDefaults, this issue can often be bypassed by simply reinstalling the app—since UserDefaults is part of the app’s sandbox and gets cleared upon uninstallation, the app starts fresh, and the user can continue using it. However, Keychain is not part of the app bundle; it is managed by the iOS operating system and persists even after the app is uninstalled. Therefore, if the app crashes while trying to parse outdated or incompatible data from the Keychain, it will continue to crash even after reinstallation. In such cases, the app developer will most likely need to release a new version with the necessary fixes.

    Sample App

    In this post, we will implement a sample app that includes a trial mechanism. This means the app will be freely usable for a limited period. After that, users will need to complete certain tasks to unlock continued usage. It’s highly likely you’ve encountered similar applications before.

    The data structure that controls the trial mechanism can only be stored in the keychain for two main reasons. First, it requires a secure storage location. Second—and equally important—it must be stored in a place that retains the information even if the app is uninstalled. Otherwise, users could simply reinstall the app to reset the trial period and continue using it without restriction.

    The structure that controls trial mechanism is following:

    typealias TrialInfoLatestModel = TrialInfo
    
    protocol TrialInfoMigratable:Codable {
        var version: Int { get }
        func migrate() -> TrialInfoMigratable?
    }
    
    // Current model (v0)
    struct TrialInfo: Codable, TrialInfoMigratable, Equatable {
        var version: Int = 0
        let startDate: Date
        
        static let defaultValue = TrialInfo(startDate: Date())
        
        func migrate() -> (any TrialInfoMigratable)? {
            nil
        }
    }

    This Swift code defines a versioned data model system for TrialInfo, where TrialInfoMigratable is a protocol that allows models to specify their version and migrate to newer versions. The TrialInfo struct represents version 0 of the model, contains a startDate, and conforms to Codable, Equatable, and TrialInfoMigratable. It includes a static default value and a migrate() method that returns nil, indicating no further migration is needed. The typealias TrialInfoLatestModel = TrialInfo serves as an alias for the latest version of the model, making future upgrades easier by allowing seamless substitution with newer model versions.

    Next is review the function that handles migration placed on viewModel:

    @Suite("TrialViewModelTest", .serialized) // Serialize for avoiding concurrent access to Keychain
    struct TrialViewModelTest {
    
        @Test("loadTrialInfo when nil")
        func loadTrialInfoWhenNil() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            // When
            let info = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(info == nil)
        }
       
        @Test("Load LatestTrialInfo when previous stored TrialInfo V0")
        func loadTrialInfoWhenV0() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfo(startDate: Date.now)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 0)
        }
    }

    Basically validate then trial data has not been and has been stored. Once, we are sure that tests pass then deploy the app into the simulator.

    review

    First change on Trial Data

    The core idea behind a migration mechanism is to keep incoming changes as simple as possible.
    We now propose updating the trial data structure with two new attributes (v1).
    Installing the app for the first time with v1 will not pose any problems. However, issues may arise when the app was initially installed with version 0 (v0) and later updated to v1.
    In such cases, the app must perform a migration from v0 to v1 upon startup.

    The following changes will be made to the Trial data structure:

    typealias TrialInfoLatestModel = TrialInfoV1
    
    .....
    
    // Current model (v1)
    struct TrialInfoV1: Codable, TrialInfoMigratable {
        var version: Int = 1
        let startDate: Date
        let deviceId: String
        let userId: String
        
        static let defaultValue = TrialInfoV1(startDate: Date(), deviceId: UUID().uuidString, userId: UUID().uuidString)
        
        init(startDate: Date, deviceId: String, userId: String) {
            self.startDate = startDate
            self.deviceId = deviceId
            self.userId = userId
        }
        
        func migrate() -> (any TrialInfoMigratable)? {
            nil
        }
    }
    
    // Current model (v0)
    struct TrialInfo: Codable, TrialInfoMigratable, Equatable {
        ...
        
        func migrate() -> (any TrialInfoMigratable)? {
            TrialInfoV1(startDate: self.startDate, deviceId: UUID().uuidString, userId: UUID().uuidString)
        }
    }

    Typealias has to be set to latest version type and we have to implement the migration function that converts v0 to v1. And in the migration new type also to migration function:

        func loadTrialInfo(key: String) async -> TrialInfoLatestModel? {
            ...
            let versionedTypes: [TrialInfoMigratable.Type] = [
                TrialInfo.self
            ]
            ...
        }

    No more migration changes, no execute unit tests:

    Screenshot

    Unit tests fails mainly because v0 stored was migrated to v1. Adapt unit test and new following test:

    @Suite("TrialViewModelTest", .serialized) // Serialize for avoiding concurrent access to Keychain
    struct TrialViewModelTest {
    ...
       
        @Test("Load LatestTrialInfo when previous stored TrialInfo V0")
        func loadTrialInfoWhenV0() async throws {
           ....
            // Then
            #expect(trialInfoStored?.version == 1)
        }
        
        @Test("Load LatestTrialInfo when previous stored TrialInfo V1")
        func loadTrialInfoWhenV1() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfoV1(startDate: Date.now, deviceId: UUID().uuidString, userId: UUID().uuidString)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 1)
        }
    }

    Repeat test execution to ensure everything is operating safely and as expected.

    Next trial data

    Next change, v2, removes one of the attributes added in v1. Changes on trial data structure are following:

    typealias TrialInfoLatestModel = TrialInfoV2
    
    ...
    // Current model (v2)
    struct TrialInfoV2: Codable, TrialInfoMigratable {
        
        var version: Int = 2
        let startDate: Date
        let deviceId: String
        
        static let defaultValue = TrialInfoV2(startDate: Date(), deviceId: UUID().uuidString)
        
        init(startDate: Date, deviceId: String) {
            self.startDate = startDate
            self.deviceId = deviceId
        }
        
        func migrate() -> (any TrialInfoMigratable)? {
           nil
        }
    }
    
    // Current model (v1)
    struct TrialInfoV1: Codable, TrialInfoMigratable {
        ...
        func migrate() -> (any TrialInfoMigratable)? {
            TrialInfoV2(startDate: self.startDate, deviceId: self.deviceId)
        }
    }
    ...
    }
    
    
    
    

    Shift typealias to latest defined type and implement the migration function that transform v1 to v2. Adapt also viewmodel migration function and add v1 type to type array:

            let versionedTypes: [TrialInfoMigratable.Type] = [
                TrialInfoV1.self,
                TrialInfo.self
            ]

    Finally run the test, adapt them and add test case for v2:

        @Test("Load LatestTrialInfo when previous stored TrialInfo V2")
        func loadTrialInfoWhenV2() async throws {
            // Given
            let sut = await TrialViewModel()
            await KeychainManager.shared.deleteKeychainData(for: sut.key)
            let trialInfo = TrialInfoV2(startDate: Date.now, deviceId: UUID().uuidString)
            await sut.saveMigrated(object: trialInfo, key: sut.key)
            // When
            let trialInfoStored = await sut.loadTrialInfo(key: sut.key)
            // Then
            #expect(trialInfoStored?.version == 2)
        }

    Conclusions

    In this post, I presented a common issue encountered when working with persisted data, along with a possible solution for handling data migration.

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

    References

  • Alternative to .xcodeproj Chaos: Intro to Tuist for iOS Newbies

    Alternative to .xcodeproj Chaos: Intro to Tuist for iOS Newbies

    Tuist is a powerful yet often underutilized tool that can greatly simplify project setup, modularization, and CI workflows. For iOS development beginners, it offers a valuable opportunity to overcome the complexity and fragility of Xcode project files—especially when aiming to build scalable architectures.

    This post serves as a step-by-step guide to help developers get started with Tuist from scratch. While Tuist truly shines in large, complex projects involving multiple developers, this guide is tailored for individuals working on personal apps. The goal is to introduce a different, more structured way of managing project configurations—and to offer a glimpse into how larger, scalable projects are typically handled.

    Install Tuist

    I have installed Tuist by using homebrew, just typing floowing two commands:

    $ brew tap tuist/tuist
    $ brew install --formula tuist
    intallTuist

    That is not the only way. To learn more, please refer to the Install Tuist section in the official documentation.

    Create HelloTuist project

    Navigate to the parent folder where you want to create your iOS app project. The Tuist initialization command will create a new folder containing the necessary project files.

    To initialize a project for the first time, simply run:

    $ tuist init
    tuistinit

    You will be asked a few questions, such as whether the project is being created from scratch or if you’re migrating from an existing .xcodeproj or workspace, which platform you’re targeting, and whether you’re implementing a server.

    Since this is an introduction to Tuist, we will choose «Create a generated project.»

    Let’s take a look at the command that was generated.

    generated

    Key Point for understanding this technology, it’s important to know that we’ll be working with two projects.

    The first one, which we’ll refer to in this post as the Tuist project, is the Xcode project responsible for managing the iOS project configuration—this includes target settings, build configurations, library dependencies, and so on.

    The second one is the application project, which is the actual codebase of your app. However, note that this project scaffolder is regenerated each time certain configuration settings change.

    I understand this might sound complicated at first, but it’s a great way to separate application logic from project configuration.

    Lets take a look at the generaded Tuist project by typing following command:

    $ tuist edit

    XCode will be opened up showing to you Tuist project, with some source code, this Swift project configuration source code will be the responsilbe for generating the project scaffoling for your application (or library).

    tuistproj

    This Swift code uses the Tuist project description framework to define a project named «HelloTuist» with two main targets: an iOS app (HelloTuist) and a set of unit tests (HelloTuistTests). The main app target is configured as an iOS application with a unique bundle identifier, sources and resources from specified directories, and a custom launch screen setup in its Info.plist. The test target is set up for iOS unit testing, uses default Info.plist settings, includes test source files, and depends on the main app target for its operation.

    Lets continue with the workflow, next step is generating application project by typing:

    $ tuist generate --no-open
    tuistgenerate

    By using this command, we have created the final .xcodeproj application project. The --no-open optional parameter was specified to prevent Xcode from opening the project automatically. We will open the project manually from the command line.

    $ xed .
    xcode

    The default project folder structure is different from what we’re accustomed to when coming from classical Xcode project creation. However, it’s something we can get used to, and it’s also configurable through the Tuist project.

    Deploying the app on the simulator is useful just to verify that everything works as expect

    Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 16.55.34

    From now on, focus only on your Swift application’s source code files. It’s important to remember that the .xcodeproj file is no longer part of your base code — it is autogenerated. Whenever you need to change project configurations, edit the Tuist project and run tuist generate.

    Setup application version

    The application version and build number are two parameters—MARKETING_VERSION and CURRENT_PROJECT_VERSION—managed in the project’s build settings. To set these values, open the Tuist project using the command line.

    $ tuist edit

    And set following changes (On Tuist prject):

    public extension Project {
        static let settings: Settings = {
            let baseConfiguration: SettingsDictionary = [
                "MARKETING_VERSION": "1.2.3",
                "CURRENT_PROJECT_VERSION": "42"
            ]
            let releaseConfiguration = baseConfiguration
            return Settings.settings(base: baseConfiguration, debug: baseConfiguration, release: releaseConfiguration)
        }()
    }
    
    public extension Target {
        static let settings: Settings = {
            let baseConfiguration: SettingsDictionary = [:]
            var releaseConfig = baseConfiguration
            return Settings.settings(base: baseConfiguration, debug: baseConfiguration, release: releaseConfig)
        }()
    }

    We aim to have a unique version value across all targets. To achieve this, we’ve set the version value at the project level and assigned an empty dictionary ([:]) in the target settings to inherit values from the project settings.

    Finally, configure the settings for both the project and target structures:

    let project = Project(
        name: "HelloTuist",
        settings: Project.settings,
        targets: [
            .target(
                name: "HelloTuist",
                destinations: .iOS,
                product: .app,
                bundleId: "io.tuist.HelloTuist",
                infoPlist: .extendingDefault(with: [
                    "CFBundleDisplayName": "Tuist App",
                    "CFBundleShortVersionString": "$(MARKETING_VERSION)",
                    "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)"
                ]),
                sources: ["HelloTuist/Sources/**"],
                resources: ["HelloTuist/Resources/**"],
                dependencies: [],
                settings: Target.settings
            ),
            .target(
                name: "HelloTuistTests",
                destinations: .iOS,
                product: .unitTests,
                bundleId: "io.tuist.HelloTuistTests",
                infoPlist: .default,
                sources: ["HelloTuist/Tests/**"],
                resources: [],
                dependencies: [.target(name: "HelloTuist")]
            ),
        ]
    )

    The settings parameter is defined both at the project and target levels. Additionally, MARKETING_VERSION is linked to CFBundleShortVersionString in the Info.plist. As a result, the app will retrieve the version value from CFBundleShortVersionString.

    Once the Tuist project is set up, the application project should be regenerated.

    $ tuist generate --no-open
    rege

    And open application project adding following changes con the view:

    struct ContentView: View {
        var appVersion: String {
            let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?"
            let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?"
            return "Version \(version) (\(build))"
        }
        
        var body: some View {
            NavigationView {
                List {
                    Section(header: Text("Information")) {
                        HStack {
                            Label("App versusion", systemImage: "number")
                            Spacer()
                            Text(appVersion)
                                .foregroundColor(.secondary)
                        }
                    }
                }
                .navigationTitle("Hello Tuist!")
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    }

    Deploy app for checking that version is properly presented.

    Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 17.34.41

    As you have observed, the operations of setting the app version in the project configuration and presenting the version are decoupled. Additionally, changes made to the project are now easier to review.

    changes

    Adding library dependencies

    Another common project configuration is the addition of third-party libraries. I’m neither for nor against them—this is a religious debate I prefer not to engage in.

    For demonstration purposes, we will integrate the Kingfisher library to fetch a remote image from the internet and display it in the application’s view.

    Again, open back Tuist project by typing ‘tuist edit’ and set the library url on ‘Tuist/Package.swift’ file:

    let package = Package(
        name: "tuistHello",
        dependencies: [
            // Add your own dependencies here:
            .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "8.3.2")),
            // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies
        ]
    )

    Also set dependencies attribute array:

    let project = Project(
        name: "HelloTuist",
        settings: Project.settings,
        targets: [
            .target(
                name: "HelloTuist",
                destinations: .iOS,
                product: .app,
                bundleId: "io.tuist.HelloTuist",
                infoPlist: .extendingDefault(with: [
                    "CFBundleDisplayName": "Tuist App",
                    "CFBundleShortVersionString": "$(MARKETING_VERSION)",
                    "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)"
                ]),
                sources: ["HelloTuist/Sources/**"],
                resources: ["HelloTuist/Resources/**"],
                dependencies: [
                    .external(name: "Kingfisher")
                ],
                settings: Target.settings
            ),
            .target(
                name: "HelloTuistTests",
                destinations: .iOS,
                product: .unitTests,
                bundleId: "io.tuist.HelloTuistTests",
                infoPlist: .default,
                sources: ["HelloTuist/Tests/**"],
                resources: [],
                dependencies: [.target(name: "HelloTuist")]
            ),
        ]
    )

    Following workflow, re-generate application project by typing ‘tuist generate’.

    tuistgenfa

    … but this time something is going wrong. And is because when an external library is bein integrated iun a Tuist project we have to install it first. By typing following command app is downloaded and installed:

    $ tuist install

    After ‘tuist install’ execute ‘tuist generate –no-open’ for finally generating application project. Add following content to view:

    import Kingfisher
    import SwiftUI
    
    struct ContentView: View {
       ...
        var body: some View {
            NavigationView {
                List {
                    Section(header: Text("Information")) {
                        HStack {
                            Label("App versusion", systemImage: "number")
                            Spacer()
                            Text(appVersion)
                                .foregroundColor(.secondary)
                        }
                    }
                    KFImage(URL(string: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQyfYoLcb2WNoStJH01TT2TLAf_JbD_FhIJng&s")!)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 300, height: 300)
                        .cornerRadius(12)
                        .shadow(radius: 5)
                        .padding()
                }
                .navigationTitle("Hello Tuist!")
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    }

    Finally deploy app for checking that image is properly fetched:

    Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 18.16.20

    Finally deploy app for checking that image is properly fetched.

    Conclusions

    The first time I heard about this technology, I was immediately drawn to it. However, I must confess that I struggled at first to understand the workflow. That’s exactly why I decided to write this post.

    While Tuist might be overkill for small projects, it becomes essential for large ones—especially when you consider the number of lines of code and developers involved. After all, every big project started out small.

    Another major advantage is that changes to the project setup are decoupled from changes to the application itself. This makes them easier to review—much like comparing SwiftUI code to .xib files or storyboards.

    Who knows? Maybe Apple will one day release its own version of Tuist, just like it did with Combine and Swift Package Manager (SPM).

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

    References

  • Custom @propertyWrapper in Action

    Custom @propertyWrapper in Action

    @propertyWrapper is interesting because it demystifies an advanced Swift feature that helps encapsulate logic, reduce boilerplate, and improve code maintainability. Many developers may not fully utilize property wrappers, despite their practical applications in areas like UserDefaults management, data validation, and SwiftUI state handling (@State, @Published, etc.). By providing clear explanations, real-world examples, and best practices we will present a pair of examples where it could be interesting approach implementation by using @properyWrapper.

    Custom @propertyWrapper

    A custom property wrapper in Swift is a specialized type that allows you to add custom behavior or logic to properties without cluttering the main class or struct code. It’s a powerful feature introduced in Swift 5 that enables developers to encapsulate common property-related functionality, such as validation, transformation, or persistence, in a reusable manner.

    Custom property wrappers are particularly useful for:

    1. Encapsulating repetitive code patterns.

    2. Adding validation or transformation logic to properties1.

    3. Implementing persistence mechanisms, such as UserDefaults storage.

    4. Creating SwiftUI-compatible state management solutions.

    Clamped type example

    First example is a clamped type, that means an int ranged value:

    @propertyWrapper
    struct Clamped<Value: Comparable> {
        private var value: Value
        let range: ClosedRange<Value>
    
        init(wrappedValue: Value, _ range: ClosedRange<Value>) {
            self.range = range
            self.value = range.contains(wrappedValue) ? wrappedValue : range.lowerBound
        }
    
        var wrappedValue: Value {
            get { value }
            set { value = min(max(newValue, range.lowerBound), range.upperBound) }
        }
    }
    
    // Uso del Property Wrapper
    struct Player {
        @Clamped(wrappedValue: 50, 0...100) var health: Int
    }
    
    var player = Player()
    player.health = 120
    print(player.health) // Output: 100 (se ajusta al máximo del rango)

    The Clamped property wrapper ensures that a property’s value remains within a specified range. It takes a ClosedRange<Value> as a parameter and clamps the assigned value to stay within the defined bounds. When a new value is set, it uses min(max(newValue, range.lowerBound), range.upperBound) to ensure the value does not go below the lower bound or exceed the upper bound. If the initial value is outside the range, it is automatically set to the lower bound. This makes Clamped useful for maintaining constraints on variables that should not exceed predefined limits.

    In the Player struct, the health property is wrapped with @Clamped(wrappedValue: 50, 0...100), meaning its value will always stay between 0 and 100. If we set player.health = 120, it gets clamped to 100 because 120 exceeds the maximum allowed value. When printed, player.health outputs 100, demonstrating that the wrapper effectively enforces the constraints. This approach is particularly useful in scenarios like game development (e.g., keeping player health within a valid range) or UI elements (e.g., ensuring opacity remains between 0.0 and 1.0).

    UserDefaults example

    Second example is a UserDefaults sample:

    @propertyWrapper
    struct UserDefault<T> {
        let key: String
        let defaultValue: T
    
        var wrappedValue: T {
            get {
                return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
            }
            set {
                UserDefaults.standard.set(newValue, forKey: key)
            }
        }
    }
    
    @MainActor
    struct Settings {
        @UserDefault(key: "username", defaultValue: "Guest")
        static var username: String
    }
    
    Settings.username = "SwiftUser"
    print(Settings.username) // Output: "SwiftUser"

    This code defines a property wrapper, UserDefault<T>, which allows easy interaction with UserDefaults in Swift. The wrapper takes a generic type T, a key for storage, and a defaultValue to return if no value is found in UserDefaults. The wrappedValue property is used to get and set values in UserDefaults: when getting, it retrieves the stored value (if available) or falls back to the default; when setting, it updates UserDefaults with the new value.

    The Settings struct defines a static property username using the @UserDefault wrapper. This means Settings.username reads from and writes to UserDefaults under the key "username". When Settings.username = "SwiftUser" is set, the value is stored in UserDefaults. The subsequent print(Settings.username) retrieves and prints "SwiftUser" since it was saved, demonstrating persistent storage across app launches.

    Conclusions

    By using custom property wrappers, you can significantly reduce boilerplate code and improve the modularity and reusability of your Swift projects

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

    References

  • From Zero to SOAP

    From Zero to SOAP

    SOAP (Simple Object Access Protocol) is often chosen over REST or GraphQL for scenarios requiring high security, reliability, and formal contracts, particularly in enterprise environments. Its built-in support for WS-Security, strong typing, and detailed error handling makes it ideal for industries like finance or healthcare that demand strict compliance and complex, stateful transactions. SOAP’s WSDL provides a clear, formal contract between client and server, ensuring interoperability across different platforms and legacy systems. However, REST and GraphQL are generally preferred for modern web applications due to their simplicity, flexibility, and lower overhead, making them more suitable for mobile or web-based services where performance and ease of use are prioritized. The choice ultimately depends on the specific requirements of the project, with SOAP excelling in structured, secure, and complex use cases.

    In previous post we have explores API REST, GraphQL and Websocket network aplicatuib interfaces, now is turn of SOAP. In this post we are going to develop a simple dockerized NodeJS SOAP server that offers an add callculation service.

    Addition SOAP Server

    First step is to build a SOAP server that will implement arithmethic add operation. The server technology used by its simpicity is a Dockerized NodeJS server. Let’s create a nodeJS server from scratch

    npm init -y

    Next install server dependencies for soap and express

    npm install soap express

    This is SOAP server itself:

    const express = require('express');
    const soap = require('soap');
    const http = require('http');
    
    const app = express();
    
    const service = {
      MyService: {
        MyPort: {
          AddNumbers: function (args) {
            const aValue = parseInt(args.a , 10);
            const bValue = parseInt(args.b , 10);
            console.log(' args.a + args.b',  aValue + bValue);
            return { result: aValue + bValue };
          },
        },
      },
    };
    
    const xml = `
    <definitions name="MyService"
      targetNamespace="http://example.com/soap"
      xmlns="http://schemas.xmlsoap.org/wsdl/"
      xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
      xmlns:tns="http://example.com/soap"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    
      <message name="AddNumbersRequest">
        <part name="a" type="xsd:int"/>
        <part name="b" type="xsd:int"/>
      </message>
      
      <message name="AddNumbersResponse">
        <part name="result" type="xsd:int"/>
      </message>
    
      <portType name="MyPort">
        <operation name="AddNumbers">
          <input message="tns:AddNumbersRequest"/>
          <output message="tns:AddNumbersResponse"/>
        </operation>
      </portType>
    
      <binding name="MyBinding" type="tns:MyPort">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="AddNumbers">
          <soap:operation soapAction="AddNumbers"/>
          <input>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </input>
          <output>
            <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
          </output>
        </operation>
      </binding>
    
      <service name="MyService">
        <port name="MyPort" binding="tns:MyBinding">
          <soap:address location="http://localhost:8000/wsdl"/>
        </port>
      </service>
    </definitions>
    `;
    
    const server = http.createServer(app);
    server.listen(8000, () => {
      console.log('SOAP server running on http://localhost:8000/wsdl');
    });
    
    soap.listen(server, '/wsdl', service, xml);
    

    This Node.js script creates a SOAP web service using the express and soap libraries. The service, named MyService, exposes a single operation called AddNumbers, which takes two integer arguments (a and b), adds them together, and returns the result. The WSDL (Web Services Description Language) XML definition specifies how clients can interact with this service, including the request and response message structures, binding details, and the service endpoint (http://localhost:8000/wsdl). The server listens on port 8000, and when a SOAP request is received, it executes the AddNumbers operation and logs the sum to the console.

    The soap.listen() function attaches the SOAP service to the HTTP server, making it accessible to clients that send SOAP requests. The AddNumbers function extracts and parses the input arguments from the request, computes their sum, and returns the result in a SOAP response. This setup enables interoperability with SOAP-based clients that conform to the specified WSDL schema.

    Lets dockerize server:

    # Official image for Node.js
    FROM node:18
    
    # Fix working directory
    WORKDIR /app
    
    # Copy necessary files
    COPY package.json package-lock.json ./
    RUN npm install
    
    # Copy rest of files
    COPY . .
    
    # Expose server port
    EXPOSE 8000
    
    # Command for executing server
    CMD ["node", "server.js"]

    This Dockerfile sets up a containerized environment for running a Node.js application. It starts with the official Node.js 18 image as the base. The working directory inside the container is set to /app. It then copies the package.json and package-lock.json files into the container and runs npm install to install dependencies. After that, it copies the rest of the application files into the container. The file exposes port 8000, which is likely used by the Node.js server. Finally, it specifies the command to run the server using node server.js when the container starts.

    Build the image from command line:

    docker build -t soap-server .

    … and execute the image:

    docker run -p 8000:8000 soap-server

    Server ready, now is turn of Swift iOS App client.

    iOS SOAP Client

    iOS app client UI interface is just two textfield for fill in the input operator for the addition, a button for executing the operation and finally a text labed that presents the results.

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    When button is tapped then callSoaService  function is being called:

    import SwiftUI
    
    struct ContentView: View {
        @State private var result: String = ""
        @State private var aStr: String = ""
        @State private var bStr: String = ""
    
        var body: some View {
            VStack {
                Group {
                    TextField("a", text: $aStr)
                    TextField("b", text: $bStr)
                }
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
    
                Button("Do remote addition") {
                    guard let aInt = Int(aStr), let bInt = Int(bStr) else {
                        return
                    }
                    callSoapService(a: aInt, b: bInt) { response in
                        DispatchQueue.main.async {
                            self.result = response
                        }
                    }
                }            .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
                
                Text("a+b= \(result)")
                    .font(.title)
                    .padding()
            }
        }

    This Swift function callSoapService(a:b:completion:) sends a SOAP request to a web service at http://localhost:8000/wsdl, passing two integers (a and b) to the AddNumbers method. It constructs an XML SOAP message, sends it via an HTTP POST request, and processes the response asynchronously using URLSession. The response is parsed to extract the result from the <tns:result> tag using a regular expression in the extractResult(from:) function, returning it via a completion handler.

    If the web service is running correctly, it should return the sum of a and b. However, the function may fail if the server is unavailable, the response format changes, or if the SOAP service requires additional headers. Also, the regular expression parsing method may not be robust against namespace variations.

    Build and run iOS app:

     

    Conclusions

    SOAP does not provide the flexibility that provides a regular API REST or GraphQL, but for those scenarios wher do we need a very strict API contracts is quite suitable technology.

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

  • Tired of Repeating Configs in Every Target?

    Tired of Repeating Configs in Every Target?

    Centralizing configuration parameters across multiple iOS targets is a valuable approach, especially in larger or modularized projects. Maintaining separate settings for each target often leads to duplication, inconsistency, and errors. Developers frequently struggle to keep build settings, API endpoints, feature flags, and environment variables in sync across targets such as staging, production, or app extensions.

    By demonstrating how to structure and manage these settings in a clean, scalable way—using tools like xcconfig files or centralized Swift structs—you can enhance maintainability, reduce bugs, and promote best practices in professional iOS development.

    In this post, we’ll walk through an example of centralizing the project and build version across multiple targets.

    Note: The goal here is not to convince you to centralize all configurations, but to show you how to do it effectively if your project requires it.

    Multiple version values

    One common problem when having more than one target app is managing multiple version values across different targets for the same app version.

    In this case, MARKETING_VERSION and CURRENT_PROJECT_VERSION are defined in three places: the project build settings and each target’s build settings. We want to define them at the project level, and have each target inherit these values from the project.

    To do this, select the CenterXCodeConfigs target:

    And replace 1 by $(CURRENT_PROJECT_VERSION), and also

    1.0 by $(MARKETING_VERSION). Switch to project build settings:

    Now, fill in the current project version and marketing version with the desired values, then switch back to the CenterXCodeConfigs target’s build settings.

    Voilà! Values are now inherited from the project settings. Repeat the same operation for the AlternativeApp target.

    Conclusions

    In this post, I presented how to centralize common settings values across all targets. You can find source code used for writing this post in following repository