Autor: admin

  • 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.

    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

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

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

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

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

    Apple Sign in

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

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

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

    Dockerized Node.JS server side

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

    npm init -y

    Server.js code is following:

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

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

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

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

    Server is dockerized:

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

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

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

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

    Server ready for receiving requests…

    Client iOS Apple Sign in app

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

    The next step is the client code itself:

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

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

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

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

    Apple Sign in integration

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

    review

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

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

    Conclusions

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

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

    References

  • Dependency Injection implementations in Swift

    Dependency Injection implementations in Swift

    How to use dependency injection both manually and with a library like Swinject is valuable because it helps developers understand the core principles behind DI, such as decoupling and testability, before introducing them to more scalable, flexible solutions. By comparing both approaches, you empower readers to make informed architectural decisions based on project complexity and team needs. It appeals to a broad audience—from beginners learning the basics to experienced developers seeking to streamline their code using frameworks—and highlights the real-world tradeoffs between control and convenience, making it a practical and educational resource.

    DIP-Dependency injection principle

    Dependency injection is a software design principle in which an object or function receives the resources or dependencies it needs from external sources rather than creating them itself, promoting loose coupling and greater flexibility in code. By separating the responsibility of constructing dependencies from their usage, dependency injection makes programs easier to test, maintain, and modify, since dependencies can be swapped or mocked without changing the dependent code. This approach is closely related to the inversion of control principle, as it shifts the creation and management of dependencies to an external entity, often called an injector or container, allowing for more modular and configurable systems.

    For our example the interface will be ‘UserService’:

    protocol UserService {
        func fetchUsers() -> [User]
    }
    
    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 1, name: "Alice"),
                User(id: 2, name: "Bob")
            ]
        }
    }
    

    This is how View model uses the UserService interface:

    class UserListViewModel: ObservableObject {
        @Published var users: [User] = []
    
        private let userService: UserService
    
        init(userService: UserService) {
            self.userService = userService
            loadUsers()
        }
    
        func loadUsers() {
            self.users = userService.fetchUsers()
        }
    }

    The view that presents user list:

    struct UserListView: View {
        @ObservedObject var viewModel: UserListViewModel
    
        var body: some View {
            List(viewModel.users) { user in
                Text(user.name)
            }
        }
    }

    …but where is dependency injection implemented? You’re probably thinking that right now. Hold on a sec…

    @main
    struct ManualDIApp: App {
        var body: some Scene {
            WindowGroup {
                let userService = DefaultUserService()
                let viewModel = UserListViewModel(userService: userService)
                UserListView(viewModel: viewModel)
            }
        }
    }
    

    At that point in the code, an instance of DefaultUserService—which implements the UserService protocol—is being injected into the viewModel.

    Dependency injection by using Swinject

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    First thing to do is add ‘https://github.com/Swinject/Swinject’ as SPM package:

    Look out! Add Swinject to SwinjectDI target, but Swinject-Dynamic to none. I faced compilations issues due to that.

    Using a dependency injection library like Swinject becomes especially beneficial in larger or more complex iOS applications where managing dependencies manually can become tedious, error-prone, and hard to scale. Libraries automate the resolution and lifecycle of dependencies, support advanced features like scopes, circular dependencies, and conditional bindings, and reduce boilerplate code—making them ideal when your project has many services, view models, or interconnected modules. They also promote consistency and cleaner architecture across teams, especially in projects following patterns like MVVM or Clean Architecture, where dependency graphs can quickly grow intricate.

    We have to declare a new component that will be responsible for resolving dependencies:

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    import Swinject
    
    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self) { _ in DefaultUserService() }
            container.register(UserListViewModel.self) { r in
                UserListViewModel(userService: r.resolve(UserService.self)!)
            }
        }
    }
    

    The code defines a singleton dependency injection container using the Swinject library to manage and resolve dependencies within an iOS application. The DIContainer class initializes a shared Container instance and registers two types: UserService, which is mapped to its concrete implementation DefaultUserService, and UserListViewModel, which depends on UserService and retrieves it from the container using resolution. By centralizing the creation of these objects, the code promotes loose coupling, testability, and maintainability, while Swinject handles the instantiation and dependency resolution automatically.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModel = DIContainer.shared.container.resolve(UserListViewModel.self)!
                UserListView(viewModel: viewModel)
            }
        }
    }

    To implement dependency injection, we simply call the resolver to fetch the appropriate instance to be injected into UserListView. Notice that UserListViewModel also depends on UserService, but this dependency is also resolved by the DIResolver. In conclusion, we can observe that the lines of code required to construct the dependency stack have been reduced to a single line.

    Handling different Protocol implementations

    What we explained in the previous section covers most cases where dependency injection needs to be implemented. However, what happens when we have different protocol implementations? For example, consider a scenario where the same view is used in different application flows, but the data sources differ—one fetches data from a database, while the other uses a REST API.

    class DefaultUserService: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 1, name: "Alice")]
        }
    }
    
    class DefaultUserServiceV2: UserService {
        func fetchUsers() -> [User] {
            return [User(id: 2, name: "Charlie")]
        }
    }
    

    We now have two classes that implement the UserService protocol. The following changes are required to build the dependency injection stack:

                // Screen 1
                let service1 = DefaultUserService()
                let viewModel1 = UserListViewModel(userService: service1)
    
                // Screen 2
                let service2 = DefaultUserServiceV2()
                let viewModel2 = UserListViewModel(userService: service2)

    View model is the same what it differs is the injected userService.

    Dependency injection by using Swinject

    The dependency injection stack usually consists of the same set of dependencies. This consistency is why third-party libraries like Swinject are beneficial—they take advantage of this common pattern.

    However, occasionally, you may encounter a rare case in your app where the dependency stack for a particular screen needs to be set up differently—for instance, when the data source differs.

    Here’s how the DIContainer resolves dependencies:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { r in
                let service = r.resolve(UserService.self, name: "v1")!
                return UserListViewModel(userService: service)
            }
    
            container.register(UserListViewModel.self, name: "v2") { r in
                let service = r.resolve(UserService.self, name: "v2")!
                return UserListViewModel(userService: service)
            }
        }
    }

    The solution was implemented by registering the instance type with a tag name. When implementing dependency injection, we need to provide an additional name tag parameter.

    @main
    struct SwinjectDIApp: App {
        var body: some Scene {
            WindowGroup {
                let viewModelV1 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v1")!
                UserListView(viewModel: viewModelV1)
                
    //            let viewModelV2 = DIContainer.shared.container.resolve(UserListViewModel.self, name: "v2")!
    //            UserListView(viewModel: viewModelV2)
            }
        }
    }

    In the previous code chunk, "v1" is hardcoded, but it should be dynamic. Ideally, it should instantiate either viewModelV1 or viewModelV2 depending on the situation.

    Unit tests

    Dependency injection in unit testing typically involves injecting a mock that implements a protocol, allowing for deterministic and controlled responses.

    import Foundation
    @testable import ManualDI
    
    class DefaultUserServiceMock: UserService {
        func fetchUsers() -> [User] {
            return [
                User(id: 99, name: "Mocked User")
            ]
        }
    }

    Unit tests will look something like this:

    import Testing
    @testable import ManualDI
    
    struct ManualDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let mock = DefaultUserServiceMock()
            let viewModel1 = UserListViewModel(userService: mock)
            
            #expect(viewModel1.users.count == 0)
        }
    
    }

    Dependency injection by using Swinject

    Unit test will have to be implemented in following way:

    import Testing
    import Swinject
    @testable import SwinjectDI
    
    struct SwinjectDITests {
    
        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let testContainer = Container()
            
            testContainer.register(UserService.self) { _ in DefaultUserServiceMock() }
    
             testContainer.register(UserListViewModel.self) { r in
                 UserListViewModel(userService: r.resolve(UserService.self)!)
             }
    
             let viewModel = testContainer.resolve(UserListViewModel.self)!
            #expect(viewModel.users.first?.name == "Mocked User")
        }
    }

    Never use the app-wide DIContainer.shared in tests — always use a local test container so you can inject mocks safely and independently.

    @Injected properly wrapper

    One more thing… By using following property wrapper:

    import Swinject
    
    @propertyWrapper
    struct Injected<T> {
        private var service: T
    
        init(name: String? = nil) {
            if let name = name {
                self.service = DIContainer.shared.container.resolve(T.self, name: name)!
            } else {
                self.service = DIContainer.shared.container.resolve(T.self)!
            }
        }
    
        var wrappedValue: T {
            service
        }
    }

    DIContiner keeps more simplified:

    class DIContainer {
        static let shared = DIContainer()
        let container: Container
    
        private init() {
            container = Container()
    
            container.register(UserService.self, name: "v1") { _ in DefaultUserService() }
            container.register(UserService.self, name: "v2") { _ in DefaultUserServiceV2() }
    
            container.register(UserListViewModel.self, name: "v1") { _ in UserListViewModel() }
            container.register(UserListViewModel.self, name: "v2") { _ in UserListViewModel() }
        }
    }

    And also viewmodel:

     class UserListViewModel: ObservableObject {
         @Published var users: [User] = []
    
         @Injected(name: "v1") private var userService: UserService
    
         func loadUsers() {
             self.users = userService.fetchUsers()
         }
     }

    Conclusions

    I did not try to convince you how useful Dependency Injection is; you can easily find information about it on the internet. Instead, I aim to show how, with a third-party library like Swinject, the process of setting up the Dependency Injection stack can be simplified.

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

    References

  • Storing in the Sky: iCloud Integration for iOS

    Storing in the Sky: iCloud Integration for iOS

    This post.demystifies a powerful yet often underused feature in the Apple ecosystem. Many developers find iCloud integration—whether through CloudKit, key-value storage, or iCloud Drive—intimidating due to scattered documentation and complex setup. By offering a clear, beginner-friendly guide with a working example, you not only fill a common knowledge gap but also empower others to build more seamless, cloud-synced experiences across devices. It’s a great way to share practical knowledge, boost your credibility, and contribute to best practices in the iOS dev community.

    iCloud

    iCloud is Apple’s cloud-based storage and computing service that allows users to securely store data such as documents, photos, music, app data, and backups across all their Apple devices. It provides a seamless way to keep content in sync, making it accessible from iPhones, iPads, Macs, and even Windows PCs. With services like iCloud Drive, iCloud Photos, and iCloud Backup, users benefit from automatic data management and recovery options, which enhances their overall experience with Apple’s ecosystem.

    For app developers, integrating iCloud offers a range of benefits that can significantly improve user engagement and satisfaction. By using iCloud technologies such as CloudKit, developers can enable real-time data synchronization and seamless transitions between devices. For instance, a note taken on an iPhone can instantly appear on a Mac or iPad without requiring manual uploads or additional login steps. This functionality not only enhances user convenience but also opens doors for multi-device collaboration and continuity in usage.

    Moreover, iCloud integration can simplify backend infrastructure for developers. With CloudKit, developers don’t need to manage their own servers for syncing user data — Apple handles the storage, security, and data transfer. This reduces development time and operational overhead, while still providing users with fast, secure, and reliable cloud features. It also adds credibility to the app by aligning it with Apple’s high standards for privacy and performance, making iCloud integration a smart and strategic choice for apps within the Apple ecosystem.

    Setup iCloud on simulator

    For start working we need to fulfill 2 basic requirement: First one is having an iOS Development (or Enterprise) account for having access to iCloud console and second in iOS Simulator (or real device) be sure that you have sign in your Apple development account:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 10.30.52

    Last but not least, be sure that iCloud Drive, Sync this iPhone switch is on:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 10.31.29

    iOS Ranking app

    The app we are going to implement to demonstrate iCloud usage will allow users to enter their name and a point value. This app will be distributed across multiple user devices, enabling each user to submit their name and score. It will also display a global ranking based on the collected data.

    Once we have created our blank iOS project, on target signing & capabilities add iCloud:

    Add a new container:

    Type its container name, has to be prefixed by iCloud. :

    Ready:

    To update your app when changes occur in iCloud, you need to handle silent push notifications. By default, enabling the iCloud capability also includes Push Notifications. However, Background Modes are not enabled automatically—so be sure to add the Background Modes capability and check the «Remote notifications» option.

    For source code app we are going to focus only in CloudkitManager, view is very simple and doest apport too much. Nevertheless you will find code respository GitHub link at the end of the post:

    import CloudKit
    import Foundation
    
    
    class RankingViewModel: ObservableObject {
        @Published var scores: [PlayerScore] = []
        private var database = CKContainer(identifier: "iCloud.jca.iCloudRanking").publicCloudDatabase
        
        init() {
            fetchScores()
            setupSubscription()
    
            NotificationCenter.default.addObserver(
                forName: .cloudKitUpdate,
                object: nil,
                queue: .main
            ) { _ in
                self.fetchScores()
            }
        }
    
        func fetchScores() {
            let query = CKQuery(recordType: "Score", predicate: NSPredicate(value: true))
            let sort = NSSortDescriptor(key: "points", ascending: false)
            query.sortDescriptors = [sort]
    
            database.perform(query, inZoneWith: nil) { records, error in
                DispatchQueue.main.async {
                    if let records = records {
                        self.scores = records.map { PlayerScore(record: $0) }.sorted { $0.points > $1.points }
                        print("Fetching successfull")
                    } else if let error = error {
                        print("Error fetching scores: \(error.localizedDescription)")
                    }
                }
            }
        }
    
        func addScore(name: String, points: Int) {
            let record = CKRecord(recordType: "Score")
            record["name"] = name as CKRecordValue
            record["points"] = points as CKRecordValue
    
            database.save(record) { _, error in
                if let error = error {
                    print("Error saving score: \(error.localizedDescription)")
                } else {
                    print("Saving successfull")
                    DispatchQueue.main.async { [weak self] in
                        self?.localAddScore(record: record)
                    }
                }
            }
        }
        
        private func localAddScore(record: CKRecord) {
            
            scores.append(PlayerScore(record: record))
            scores = scores.sorted { $0.points > $1.points }
        }
        
        func setupSubscription() {
            let subscriptionID = "ranking-changes"
    
            let predicate = NSPredicate(value: true)
            let subscription = CKQuerySubscription(
                recordType: "Score",
                predicate: predicate,
                subscriptionID: subscriptionID,
                options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
            )
    
            let notificationInfo = CKSubscription.NotificationInfo()
            notificationInfo.shouldSendContentAvailable = true  // Silent
            subscription.notificationInfo = notificationInfo
    
            database.save(subscription) { returnedSub, error in
                if let error = error {
                    print("❌ Subscription error: \(error.localizedDescription)")
                } else {
                    print("✅ Subscription saved!")
                }
            }
        }
    }

    This Swift code defines a RankingViewModel class that interfaces with Apple’s CloudKit to manage a leaderboard-style ranking system. It fetches, updates, and stores player scores in an iCloud public database (iCloud.jca.iCloudRanking) using CloudKit. When the class is initialized, it automatically retrieves existing scores from CloudKit and sets up a subscription to receive real-time updates when scores are added, modified, or deleted. It also listens for a custom cloudKitUpdate notification and refetches scores when triggered. All fetched scores are stored in the @Published array scores, allowing SwiftUI views observing this view model to update dynamically.

    The fetchScores() function queries the CloudKit database for records of type «Score», sorting them by the number of points in descending order. These records are converted into PlayerScore instances (assumed to be a custom data model) and stored in the scores array. The addScore() function allows new scores to be submitted to the database. Once saved, the new score is locally appended and sorted in the scores array via localAddScore(). Additionally, the setupSubscription() method ensures the app receives silent push notifications when there are any changes to the «Score» records in CloudKit, keeping the leaderboard data synchronized across devices.

    When we deploy:

    Simulator Screenshot - iPhone 16 Pro - 2025-04-24 at 11.50.47

    Issue, new ranking is not updated and we can read on Console log:

    For fixing that we have to make a few adjustments on iCloud Console.

    iCloud Console

    For having access to iCloud Console, just type ‘https://icloud.developer.apple.com/dashboard’ on your favourite browser and login with your Apple Developer (or Enterprise) account. Later on select the the iCloud containter that the app is being used:

    First step is creating a Record Type for taking a look at the recently uploaded user data:

    Next step is adding record fields (name and points):

    For being able to retrieve data from console we have to create a Querable Index on recordName field from Score Record Type:

    Now is time for checking previous stored data:

    For retrieve data from device, we have to create a Sortable Index for points filed in Score Record Type:

    When we deploy iOS app on a real device:

    screenshot

    Finally…

    For final validation of the iOS app concept, I deployed the app on two different physical devices. As demonstrated in the video, when a ranking is submitted on one device, the ranking list is updated almost instantly on the other device.

    Conclusions

    From a programming point of view, working with iCloud is relatively straightforward. What I’ve found a bit cumbersome, however, is setting up the iCloud Console. Overall, though, using iCloud is a good idea if you need to share data across all instances of your app.

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

    References

    • iCloud

      Apple Developer Documentation

  • Inside the iOS Sandbox: Managing Files and Folders

    Inside the iOS Sandbox: Managing Files and Folders

    Sandboxing in iOS is a foundational security mechanism that isolates each app in its own secure environment. This isolation prevents unauthorized access to system resources and user data, ensuring that apps cannot interfere with one another.

    For developers, understanding how to manage files and directories within this sandbox is crucial. It determines how and where persistent data, user-generated content, and temporary files are stored—directly affecting app functionality, user privacy, and compliance with App Store requirements.

    The goal of this post is to demystify these concepts. By doing so, it empowers developers to build secure, reliable, and user-friendly applications that align with iOS’s strict security model while effectively leveraging the available file system APIs.

    The Sandbox

    In iOS, a sandbox is a security mechanism that restricts apps to their own designated area, preventing them from accessing files, resources, or data belonging to other apps or the system without explicit permission. This isolation ensures stability, security, and privacy for users.

    Key Features of iOS Sandbox:

    1. App Isolation

      • Each app runs in its own sandboxed environment with its own directory for files.

      • Apps cannot directly read or modify files from other apps.

    2. Controlled Access to System Resources

      • Apps must request permissions (via entitlements or user consent) to access sensitive data like:

        • Contacts (Contacts.framework)

        • Photos (PHPhotoLibrary)

        • Location (CoreLocation)

        • Camera & Microphone (AVFoundation)

    3. File System Restrictions

      • Apps can only write files in their own sandbox directories, such as:

        • Documents/ (user-generated content, backed up by iTunes/iCloud)

        • Library/ (app support files, some backed up)

        • Caches/ (temporary files, can be purged by the system)

        • tmp/ (short-lived files, not backed up)

    4. No Direct Hardware or Kernel Access

      • Apps interact with hardware (e.g., GPU, sensors) only through Apple’s frameworks.

      • No root-level system modifications are allowed (unlike jailbroken devices).

    5. Inter-App Communication (Limited & Controlled)

      • Apps can share data only via:

        • URL Schemes (custom deep links like myapp://)

        • App Groups (shared containers for related apps)

        • UIActivityViewController (share sheets)

        • Universal Clipboard (limited-time data sharing)

    Why Does iOS Use a Sandbox?

    • Security: Prevents malware from spreading or tampering with other apps.

    • Privacy: Ensures apps access only permitted user data.

    • Stability: Crashes or bugs in one app don’t affect others.

    Example: Accessing the Sandbox in Code

    To get an app’s sandbox directory in Swift:

    struct PeopleView: View {
        @StateObject var viewModel = PeopleViewModel()
        
        var body: some View {
            NavigationView {
                ...
            }.onAppear {
                if let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
                    print("📂 Document Directory: \(documentsPath.path)")
                }
            }
        }
    }

    The snippet is used to retrieve and print the path to the app’s Documents directory on an iOS or macOS device. Its parent folder is the sandbox root folder for your current app.

    Exceptions to the Sandbox:

    • System apps (e.g., Messages, Mail) have broader privileges.

    • Jailbroken devices bypass sandbox restrictions (but violate Apple’s policies).

    The sandbox is a core reason iOS is considered more secure than open platforms. Developers must work within its constraints while using Apple’s APIs for permitted interactions.

    Our privacy is compromised the moment a malicious app can access another app’s sandbox. Theoretically, this kind of sandbox breach hasn’t been documented on iOS—at least not to my knowledge. However, the video «Broken Isolation – Draining Your Credentials from Popular macOS Password Managers« by Wojciech Reguła (NSSpain 2024) demonstrates how, on macOS, a malicious app can gain access to the sandboxes of other apps that store user passwords—such as NordPass, KeePass, Proton Pass, and even 1Password.

    Sandbox data container folders

    Each iOS app has its own container directory with several subdirectories. Here’s a breakdown of the key folders and their purposes:

    1. Documents

    • Path: .../Documents/

    • Purpose: Stores user-generated content or data that should persist and be backed up to iCloud.

    • Example: Saved notes, PDFs, exported data.

    • Backup: ✅ Included in iCloud/iTunes backups.

    • Access: Read/Write.

    2. Library

    • Path: .../Library/

    • Purpose: Stores app-specific files and configuration data.

      It has two main subfolders:

      • Preferences

        • .../Library/Preferences/

        • Stores user settings (e.g., using UserDefaults).

        • Managed automatically by the system.

      • Caches

        • .../Library/Caches/

        • Used for data that can be regenerated (e.g., image cache).

        • Not backed up, and iOS may delete files here when space is low.

        • ⚠️ Don’t store critical data here.

    4. tmp

    • Path: .../tmp/

    • Purpose: Temporary files your app doesn’t need to persist between launches.

    • Backup: ❌ Not backed up.

    • Auto-clean: iOS may clean this directory at any time.

    • Access: Read/Write.

    Summary Table

    FolderPurposePersistentBacked UpiOS May Delete
    App BundleApp code and resources
    DocumentsUser data/files
    Library/PreferencesApp settings (UserDefaults)
    Library/CachesCached data (non-critical)
    tmpTemporary files

     

     

    Files operations

    For this section, we have developed a sample iOS application that performs storage operations using files. The app displays an empty list with an «Add» button in the navigation bar. Each time the button is pressed, a new person is added to the list. The list of people serves as the model and is persisted as a .json file.

    When we deploy on simulator (or real device):

    The component that handles files operations:

    class FileManagerHelper {
        static let shared = FileManagerHelper()
        
        private let fileName = "people.json"
        
        private var fileURL: URL {
            let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            return documents.appendingPathComponent(fileName)
        }
    
        func save(_ people: [Person]) {
            do {
                let data = try JSONEncoder().encode(people)
                try data.write(to: fileURL)
            } catch {
                print("Error saving file: \(error)")
            }
        }
        
        func load() -> [Person] {
            do {
                let data = try Data(contentsOf: fileURL)
                let people = try JSONDecoder().decode([Person].self, from: data)
                return people
            } catch {
                print("Error reading file: \(error)")
                return []
            }
        }
        
        func deleteFile() {
            do {
                try FileManager.default.removeItem(at: fileURL)
            } catch {
                print("Error deleting file: \(error)")
            }
        }
    }

    FileManagerHelper is a singleton utility that manages saving, loading, and deleting a JSON file named people.json in the app’s documents directory. It provides methods to encode an array of Person objects into JSON and save it to disk (save), decode and return the array from the saved file (load), and remove the file entirely (deleteFile). It handles errors gracefully by catching exceptions and printing error messages without crashing the app.

    Conclusions

    With that post, I just intended to give you an overview and demonstrate how easy it is to deal with file persistence as well.

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

    References

  • Switching App Language on the Fly in Swift

    Switching App Language on the Fly in Swift

    Dynamically switching languages in views and using snapshot testing for multilingual validation it addresses a common challenge in global app development: ensuring seamless localization. Developers often struggle with updating UI text instantly when users change languages, and validating UI consistency across multiple locales can be tedious.

    We will cover dynamic language switching for streamlining the user experience, on the other side snapshot testing ensures visual accuracy without manual verification.

    Multilanguage iOS app

    After creating an iOS sample we are going to include the language catalog:

    Next step is adding a new language:

    For this example we have chosen Spanish:

    Next step we start to fill in string catalog for all languages defined:

    Language controller

    For implementing languange controller we will make use of Observer pattern, that means that this class state changes then all views subscribed to it will get notified and will be refreshed automatically:

    class LocalizationManager: ObservableObject {
        @Published var locale: Locale = .current {
            didSet {
                UserDefaults.standard.set([locale.identifier], forKey: "AppleLanguages")
                UserDefaults.standard.synchronize()
            }
        }
        
        init() {
            if let preferredLanguage = UserDefaults.standard.array(forKey: "AppleLanguages")?.first as? String {
                locale = Locale(identifier: preferredLanguage)
            }
        }
        
        func toggleLanguage() {
            if locale.identifier == "en" {
                locale = Locale(identifier: "es")
            } else {
                locale = Locale(identifier: "en")
            }
        }
    }

    This Swift class, LocalizationManager, is an ObservableObject that manages the app’s language settings. It stores the current locale in UserDefaults under the key "AppleLanguages", ensuring that the language preference persists across app restarts. The locale property is @Published, so any UI elements observing it will update when the language changes. The initializer retrieves the stored language preference from UserDefaults or defaults to the system’s current locale. The toggleLanguage() method switches between English ("en") and Spanish ("es") by updating locale, which in turn updates UserDefaults. However, changing this value does not dynamically update the app’s language in real-time without restarting.

    Finally build and run application:

    Multilanguage view

    View implementation is following:

    struct ContentView: View {
        @StateObject private var localizationManager = LocalizationManager()
        
        var body: some View {
            VStack {
                Text("welcome_message".localized(with: localizationManager.locale))
                    .padding()
                
                Button(action: {
                    localizationManager.toggleLanguage()
                }) {
                    Text("change_language".localized(with: localizationManager.locale))
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            .environmentObject(localizationManager)
        }
    }

    This SwiftUI ContentView uses a @StateObject called LocalizationManager to manage language localization. It displays a localized welcome message and a button that allows users to toggle the app’s language. The Text elements use a .localized(with:) function to fetch translated strings based on the current locale from LocalizationManager. When the button is tapped, it calls toggleLanguage(), which presumably switches between different languages. The localizationManager is also injected into the SwiftUI environment using .environmentObject(), making it accessible throughout the app.

    Finally, cleaner code on view, and it is a practice that I have seen in many projects is to create an extension for finally  retrieve string translated, but this time with the add on that is parametrized with location language code:

    extension String {
        func localized(with locale: Locale) -> String {
            let language = locale.identifier
            guard let path = Bundle.main.path(forResource: language, ofType: "lproj"),
                  let bundle = Bundle(path: path) else {
                return NSLocalizedString(self, comment: "")
            }
            return bundle.localizedString(forKey: self, value: nil, table: nil)
        }
    }

    The extension adds a localized(with:) function to the String type, allowing it to return a localized version of the string based on the specified Locale. It first retrieves the locale’s language identifier and attempts to find the corresponding .lproj resource bundle in the app’s main bundle. If the bundle exists, it fetches the localized string from it; otherwise, it falls back to NSLocalizedString, which returns the string from the app’s default localization. This enables dynamic localization based on different languages.

    Snapshot testing

    Last but not least, apart from implementing a user interface that allows user to switch language when something is not propery understood on app regular usage. It opens up a new tesging scenario, we are possible to validate the same view presented in different languages, this will you make easier to detect texts that were not presented properly. 

    In Visual Regression Testing: Implementing Snapshots test on iOS post I present how to setup and implement snapshoptesting, is very easy setup. So basically we will bypass snapshot setup library and we will focus just only on sntapshots tests:

    @Suite("Snapshot tests")
    struct DynamicLanguageTests {
    
        let record = true
    
        @Test func testContentViewInEnglish() {
                let localizationManager = LocalizationManager()
                localizationManager.locale = Locale(identifier: "en")
                
                let contentView = ContentView()
                    .environmentObject(localizationManager)
                    .frame(width: 300, height: 200)
                
                let viewController = UIHostingController(rootView: contentView)
                
                assertSnapshot(
                    of: viewController,
                    as: .image(on: .iPhoneSe),
                    named: "ContentView-English",
                    record: record
                )
            }
    
        @Test  func testContentViewInSpanish() {
                let localizationManager = LocalizationManager()
                localizationManager.locale = Locale(identifier: "es")
                
                let contentView = ContentView()
                    .environmentObject(localizationManager)
                    .frame(width: 300, height: 200)
                
                let viewController = UIHostingController(rootView: contentView)
                
                assertSnapshot(
                    of: viewController,
                    as: .image(on: .iPhoneSe),
                    named: "ContentView-Spanish",
                    record: record
                )
            }
    }

    The DynamicLanguageTests struct, annotated with @Suite("Snapshot tests"), contains two test functions: testContentViewInEnglish() and testContentViewInSpanish(). Each function sets up a LocalizationManager with a specific locale (en for English, es for Spanish), creates a ContentView instance, embeds it in a UIHostingController, and captures a snapshot of the rendered UI on an iPhone SE device. The snapshots are named accordingly («ContentView-English» and «ContentView-Spanish») and compared against previously recorded images to detect unintended visual changes.

    Conclusions

    Dynamic Language Switch it could be an interesting implementation for allowing user swap view language for beter understaning., but also is more useful when we have to validate that a view is properly presente in all app languages, you can find the source code used for this post in the following repository