Autor: admin

  • 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

  • Less Fetching, More Speed: NSCache in Action

    Less Fetching, More Speed: NSCache in Action

    Implementing image caching with NSCache in iOS addresses a common real-world challenge: efficiently loading images without compromising performance or user experience. It’s a lightweight, native solution that avoids third-party dependencies—ideal for developers building lean apps. Additionally, it serves as a natural introduction to memory management concepts, such as how NSCache automatically evicts objects under memory pressure.

    This approach helps newer developers avoid common pitfalls like UI flicker or image reloads in scrollable views and sets the stage for more advanced caching strategies, including disk storage or custom loaders.

    In this guide, we’ll walk you through a simple iOS app implementation to demonstrate the process step by step.

    NSCache

    NSCache is a specialized caching class in Apple’s Foundation framework that provides a convenient way to temporarily store key-value pairs in memory. It functions similarly to a dictionary but is optimized for situations where you want the system to manage memory usage more intelligently. One of its biggest advantages is that it automatically evicts stored items when the system is under memory pressure, helping to keep your app responsive and efficient without needing manual intervention.

    Unlike regular dictionaries, NSCache is thread-safe, which means you can access and modify it from different threads without adding synchronization logic. It’s designed to work well with class-type keys, such as NSString or NSURL, and object-type values like UIImage or custom model classes. Additionally, you can set limits on how many objects it holds or how much memory it should use, and even assign a «cost» to each object (like its file size) to help the cache prioritize what to keep or remove.

    NSCache is especially useful in cases like image loading in SwiftUI apps, where images fetched from the network can be reused rather than redownloaded. However, it only stores data temporarily in memory and doesn’t support expiration dates out of the box, so you’d need to add your own logic if you want time-based invalidation. For long-term storage or persistent caching, developers often combine NSCache with disk storage strategies to create a hybrid caching system.

    This is our NSCache wrapping implementation:

    import UIKit
    
    actor DiskImageCache {
        static let shared = DiskImageCache()
        
        private let memoryCache = NSCache<NSURL, UIImage>()
        private let fileManager = FileManager.default
        private let cacheDirectory: URL
        private let expiration: TimeInterval = 12 * 60 * 60 // 12 hours
        
        init() {
            let directory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
            cacheDirectory = directory.appendingPathComponent("ImageCache", isDirectory: true)
    
            if !fileManager.fileExists(atPath: cacheDirectory.path) {
                try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
            }
        }
        
        func image(for url: URL) -> UIImage? {
            // 1. Check memory cache
            if let memoryImage = memoryCache.object(forKey: url as NSURL) {
                return memoryImage
            }
            
            // 2. Check disk cache
            let path = cachePath(for: url)
            guard fileManager.fileExists(atPath: path.path) else { return nil }
    
            // Check expiration
            if let attributes = try? fileManager.attributesOfItem(atPath: path.path),
               let modifiedDate = attributes[.modificationDate] as? Date {
                if Date().timeIntervalSince(modifiedDate) > expiration {
                    try? fileManager.removeItem(at: path)
                    return nil
                }
            }
            
            // Load from disk
            guard let data = try? Data(contentsOf: path),
                  let image = UIImage(data: data) else {
                return nil
            }
    
            memoryCache.setObject(image, forKey: url as NSURL)
            return image
        }
        
        func store(_ image: UIImage, for url: URL) async {
            memoryCache.setObject(image, forKey: url as NSURL)
            
            let path = cachePath(for: url)
            if let data = image.pngData() {
                try? data.write(to: path)
            }
        }
    
        private func cachePath(for url: URL) -> URL {
            let fileName = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString
            return cacheDirectory.appendingPathComponent(fileName)
        }
    }

    Making DiskImageCache an actor is all about thread safety — especially when you’re doing I/O (disk reads/writes) and managing a shared resource (the cache itself).

    The code defines a DiskImageCache actor that manages a two-level cache system for images, combining in-memory and disk storage to efficiently store and retrieve images. The actor is implemented as a singleton (shared), ensuring thread-safe access to its caching mechanisms. It uses NSCache for fast in-memory storage of UIImage objects keyed by their URLs, while also maintaining a disk-based cache in the app’s Caches directory under an «ImageCache» subfolder. The disk cache includes an expiration mechanism (12 hours) that automatically removes stale files based on their modification date.

    The actor provides two main methods: image(for:) to retrieve an image and store(_:for:) to save an image. When retrieving an image, it first checks the memory cache, then falls back to disk if needed, while also handling cache expiration. When storing an image, it saves to both memory and disk. The disk cache uses URL-encoded filenames derived from the image URLs to maintain unique file paths. This implementation balances performance (with quick memory access) and persistence (with disk storage), while managing resource usage through expiration and proper file system organization.

    The viewmodel for AsyncImage View component

    The ViewModel is responsible for requesting an image from ImageCache and providing it to the view via @Published. Note that the entire class is executed on the @MainActor, whereas DiskImageCache runs in a separate actor.

    The target (and project) is configured for Swift 6 with Strict Concurrency Checking set to Complete. Since the cache operates in a different isolated domain, even though the image function is not explicitly marked as async, the compiler still requires the use of the await keyword when calling it.

    import SwiftUI
    
    @MainActor
    class AsyncImageLoader: ObservableObject {
        @Published var image: UIImage?
        
        private var url: URL
    
        init(url: URL) {
            self.url = url
        }
    
        func load() async {
            if let cached = await DiskImageCache.shared.image(for: url) {
                self.image = cached
                return
            }
    
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let downloaded = UIImage(data: data) else { return }
    
                self.image = downloaded
                await DiskImageCache.shared.store(downloaded, for: url)
            } catch {
                print("Image load failed:", error)
            }
        }
    }
    

    AsyncImageView struct defines a custom view for asynchronously loading and displaying an image from a URL using an AsyncImageLoader (assumed to handle the async fetching logic). It uses a placeholder image while the actual image is being fetched, and applies a customizable image styling closure to either the loaded image or the placeholder. The loading is triggered when the view appears, using Swift’s concurrency features (Task and await). It leverages @StateObject to maintain the image loader’s lifecycle across view updates, ensuring image state persists appropriately in the SwiftUI environment.

    The AsyncImage view component

    Refactored AsyncImageLoader into a standalone component to enable better reusability.

    import SwiftUI
    
    struct AsyncImageView: View {
        @StateObject private var loader: AsyncImageLoader
        let placeholder: Image
        let imageStyle: (Image) -> Image
    
        init(
            url: URL,
            placeholder: Image = Image(systemName: "photo"),
            imageStyle: @escaping (Image) -> Image = { $0 }
        ) {
            _loader = StateObject(wrappedValue: AsyncImageLoader(url: url))
            self.placeholder = placeholder
            self.imageStyle = imageStyle
        }
    
        var body: some View {
            Group {
                if let uiImage = loader.image {
                    imageStyle(Image(uiImage: uiImage).resizable())
                } else {
                    imageStyle(placeholder.resizable())
                        .onAppear {
                            Task {
                                await loader.load()
                            }
                        }
                }
            }
        }
    }

    AsyncImageView struct defines a custom view that asynchronously loads and displays an image from a given URL. It uses an AsyncImageLoader to manage the image fetching logic. Upon initialization, the view sets up the loader with the provided URL and stores a placeholder image and an optional styling closure for the image. In the view’s body, it conditionally displays the downloaded image if available, or the placeholder image otherwise. When the placeholder is shown, it triggers the asynchronous loading of the image via a Task inside onAppear. The imageStyle closure allows the caller to customize how the image (or placeholder) is displayed, such as adding modifiers like .aspectRatio or .frame.

    The View

    Finally AsyncImageView component is integrated in ContentView in following way:

    struct ContentView: View {
        var body: some View {
            AsyncImageView(url: URL(string: "https://picsum.photos/510")!)
                .frame(width: 200, height: 200)
                .clipShape(RoundedRectangle(cornerRadius: 20))
                .shadow(radius: 5)
        }
    }

    When we deploy iOS app on a real device:

    When the app starts up, it displays a placeholder image while the actual image is being fetched. Once the image is displayed, the app is terminated. On the second startup, the app directly shows the previously fetched image.

    Conclusions

    With that post, I just intended to show how to cache images, or any other resource for making your apps more fluid.

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

    References

  • 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

  • Bitrise Magic: One-Click iOS Builds for Your QA Team

    Bitrise Magic: One-Click iOS Builds for Your QA Team

    Automating iOS app distribution for QA and reviewers using Bitrise addresses a common pain point—manually sharing builds—by streamlining the process through CI/CD. Bitrise simplifies generating signed IPAs (for ad-hoc testing), uploading to TestFlight, and deploying to third-party services like Firebase App Distribution, all while handling code signing, versioning, and notifications automatically.

    This post is a step-by-step guide to help developers set up Bitrise to generate an iOS build artifact for distribution to QA or any reviewer.

    Getting in context

    To develop this post, we need the app for internal distribution, an iOS Developer Program license from Apple, and a free Bitrise account.

    The app we will distribute internally is an iOS project called SMOC. SMOC is a head-up display speedometer and dash camera. The app builds without issues, and all unit tests pass. While this may seem obvious, it’s important to ensure that build crashes do not occur during workflow executions.

    Being a member of the Apple Developer Program is mandatory because, at some point during the Bitrise setup, you will need to connect to the Apple Developer portal with your credentials. Additionally, you will need to provide an API key (.p8 file) to Bitrise, generated in the Apple Developer portal and also the Development Certificate (.p12 file).

    Bitrise offers a 30-day free trial account that allows you to experiment with and initiate iOS app internal distribution.

    Generating Apple Developer API Keys

    Login in into your Apple developer account:

    Select User and Access. On the incoming screen, choose Integrations > App Store Connect API from the left-side menu. Under Equipment Keys, click the Add (+) button to create a new key.

    Important point: Access must be set to ‘Administrative’; I was not successful with the ‘Developer’ role. Download the API key (.p8 file) to a secure location, as we will need it to configure Bitrise. The ID Key and Issuer ID have been masked for security reasons, but make sure to take note of them because you will need them later.

    In addition to the API key (.p8 file), we also need the Apple Development Certificate (.p12 file). To obtain it, open Keychain and export the certificate. Remember the password you set during the export process, as it will be required when importing the certificate into Bitrise.

    If the certificate is not in your keychain, you can create it via Xcode. Go to Settings > Accounts > Manage Certificates, and select Apple Development. The certificate will automatically be added to your local machine’s keychain and uploaded to the Apple Developer Portal.

    General iOS Setup

    Once logged into your Bitrise account, the first step—before configuring any project—is to set up some global Bitrise configurations that will be needed later. Since our codebase is hosted on GitHub, we will connect our GitHub account.

    Now it’s time to add your API key. Go back to the previous screen, select ‘App Store Connect,’ and press the ‘Add API Key’ button.

    The name is an identifier of your choice, but the Issuer ID and Key ID are obtained when you create an API key on the Apple Developer portal. Finally:

    Following the ‘least privilege principle’, I initially created an API key with a Developer role. However, the workflow execution failed due to insufficient access rights, so I had to generate a new API key, this time with an Administrator role.

    Lastly, we need to connect the Bitrise account with our Apple Developer account, just as we did with GitHub. However, this time, we need to go to the top-right user icon menu.

    And connect with your Apple Developer (or Enterprise) account:

    iOS Project setup

    Now is the time to create a new project. Select the dashboard and click the ‘(+) New Project’ button.

    Select ‘Add your App’:

    Select ‘New project’:

    And press Next.

    Fill in the app title, mobile platform, and app bundle ID. Then, select ‘Integrations’ from the left menu.

    And just indicate which API key you are going to use.

    Go to the Dashboard and select ‘Configure’ in the project you’re working on.

    Set up the project and access. In our case, we aim to provide CI builds to team members who either do not have access to Bitrise or are not permitted to use it.

    In our case, the code repository is GitHub, so we set the URL to point to our repository.

    Bitrise authorization is not strictly necessary because the repository is public. However, if it were private, we would need to authorize access. For the «Choose Branch» step, we select «No, skip auto-detection and configure settings manually» because, although the default branch is develop, developers often distribute builds to their QA colleagues based on their working branches rather than the default branch.

    In the ‘Default configuration’, we set the project type, project (or workspace) filename, scheme, and distribution method. In our case, we are interested in distributing builds based on development to our QA team.

    Next, we select the XCode version to be used as the toolchain and build machine. Due to our account subscription, we are limited to working with only one type of machine.

    Set the app icon to add an avatar to the project, then select ‘Register a Webhook’ for me!

    Configuration finished just press ‘View CI configuration’.

    Configuration is almost complete, but not quite. Next, we need to upload the Development Certificate (.p12 file). To do this, select ‘Code Signing’ and then click ‘Add .p12 File.

    Once added:

    Even though we set the API key previously, I faced some compilation crashes that failed due to the API key, so I had to specify again which API key I was going to use.

    Project configuration is ready. Now, select workflows to create our workflow, which is also known as pipelines in other contexts.

    Workflow creation

    On workflows select ‘(+) Create Workflow’:

    Fill in the name of the workflow, as well as the workflow on which it is based. In our case, it is ‘archive_and_export_app’:

    This would be ok in case our project used Cocoapods as a dependency manager, but is our case is not.

    So we have to remove Cocoapods step:

    Save workflow:

    Workflow execution

    Setup work finished, now is time to run the workflow, press ‘Start build’ button:

    Press ‘Start build’:

    After a few minutes….

    If everything is set up correctly, the ‘Success’ message should appear. Now, press the ‘Right arrow’ icon to continue with the build distribution.

    Install and run

    We need to provide the app download URL, either by distributing a QR code or sharing the direct link. Our responsibilities as developers are complete.

    As a QA reviewer, once we scan the QR code or enter the link in the device browser, we will see the following. The setup process will only occur the first time the app is installed. Subsequent re-installations (or removal and reinstallation) will not trigger the setup process again.

    Select ‘Check compatibility’ and following alert will appear:

    Select Allow for installing configutation profile. And Open Device Settings, General:

    Select Allow for installing configutation profile. And Open Device Settings, General, VPN & Device Management:

    And select ‘Install’. Remember this setup has to be done once per device and app distributed.

    And yes, finally press install to begin installing the app on your device. The following animation will show you how the app is installed and execute:

    Conclusions

    Although the configuration process can be tedious, once it’s set up, the developer only needs to focus on executing the workflow on the desired working branch and providing the workflow or artifact distribution link to QA or any other interested reviewer. On the QA side, they simply need to click the link to install the app and start testing.

    References

    • Bitrise

      The CI/CD Platform built for Mobile DevOps

    • SMOC

      Portfolio – On board car clip camera iOS App

    • SMOC

      Apple Store

  • QR Code Scanning in iOS

    QR Code Scanning in iOS

    Scanning a URL encoded within a QR code and fetching its contents is particularly interesting because it addresses a common real-world scenario faced by developers. QR codes are widely used for sharing links, event details, and more, making it essential for iOS apps to handle them efficiently.

    This post will not only guide developers through implementing QR code scanning using native frameworks like AVFoundation but also demonstrate how to seamlessly fetch and process the retrieved URL content using URLSession.

    We will implement a client-server application where the server will provide a QR image and implement REST API services encoded within the QR code. The client will be an iOS app that scans the QR code and fetches the REST API service.

    QR-Server

    For the server, we will implement a Dockerized Node.js server to create a blank project.

    npm init -y

    Later on, we will need to integrate ‘express’ and ‘qrcode’ into the project.

    npm install express qrcode

    Server code is following:

    const express = require('express');
    const QRCode = require('qrcode');
    const os = require('os');
    
    const app = express();
    const port = 3000;
    const hostIP = process.env.HOST_IP || 'Desconocida';
    
    app.get('/', (req, res) => {
        const url = `http://${hostIP}:${port}/data`;
        QRCode.toDataURL(url, (err, qrCode) => {
            if (err) res.send('Error generating QR code');
            res.send(`<!DOCTYPE html><html><body>
            <h2>Scan the QR code:</h2>
            <img src="${qrCode}" />
            </body></html>`);
        });
    });
    
    app.get('/data', (req, res) => {
        res.json({ message: 'Hello from QR-API-Server!' });
    });
    
    app.listen(port, () => {
        console.log(`Server running on http://${hostIP}:${port}`);
    });

    This Node.js script sets up an Express.js web server that generates a dynamic QR code. When the root URL («/») is accessed, the server creates a QR code containing a URL pointing to the /data endpoint. The QR code is displayed on an HTML page with the message «Scan the QR code.»

    Accessing the /data endpoint returns a JSON object with the message: «Hello from QR-API-Server!». The server listens on port 3000, and the host IP address is either obtained from an environment variable or defaults to 'Desconocida' (Unknown) if not specified.

    Next step: set up the Dockerfile.

    # Base image for Node.js
    FROM node:14
    
    # Create and fix working dirctory
    WORKDIR /usr/src/app
    
    # Copy application files
    COPY . .
    
    # Install dependencies
    RUN npm install
    
    # Expose appliction port
    EXPOSE 3000
    
    # Command for starting server application
    CMD ["node", "server.js"]

    The Dockerfile sets up a Docker image for a Node.js application. It starts by using the official Node.js 14 base image. It then creates and sets the working directory to /usr/src/app. The application files from the local directory are copied into this working directory. Next, it installs the necessary dependencies using npm install. The image exposes port 3000, indicating that the application will listen on that port. Finally, the Docker container will run the Node.js server application by executing node server.js when started.

    Get back to command line and create docker image:

    docker build -t qr-server .

    And finally run the image:

    docker run -d -p 3000:3000 \ 
    -e HOST_IP=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -n 1) qr-server

    You need to provide the host Docker machine’s container runner IP to allow an iOS app running on a real device (due to camera scanning) to access the server.

    QR Scaner iOS App

    Client is an iOS sample Scan app designed to scan QR codes and call the service endpoint encoded within the QR code. To perform scanning, the app will require access to the camera.

    Open target build settings and fill in ‘Privacy – Camera Usage Description’. View code is following:

    struct ContentView: View {
        @State private var scannedURL: String? = nil
        @State private var dataFromAPI: String? = nil
        @State private var showAlert = false
        @State private var alertMessage = ""
    
        var body: some View {
            VStack {
                if let scannedURL = scannedURL {
                    Text("URL scanned: \(scannedURL)")
                        
                        .padding()
    
                    Button("Do API Call") {
                        Task {
                            await fetchAPIData(from: scannedURL)
                        }
                    }
                    .padding()
    
                    if let dataFromAPI = dataFromAPI {
                        Text("Data from API: \(dataFromAPI)")
                            .padding()
                    }
                } else {
                    ZStack {
                        
                        QRCodeScannerView {
                            self.scannedURL = $0
                        }
                        .edgesIgnoringSafeArea(.all)
                        Text("Scan QR code:")
                    }
    
                }
            }
            .font(.title)
            .alert(isPresented: $showAlert) {
                Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
            }
        }
    
        func fetchAPIData(from url: String) async {
            guard let url = URL(string: url) else { return }
    
            do {
                let (data, response) = try await URLSession.shared.data(from: url)
                if let result = String(data: data, encoding: .utf8) {
                    dataFromAPI = result
                }
            } catch {
                alertMessage = "Error: \(error.localizedDescription)"
                showAlert = true
            }
        }
    }

    SwiftUI code creates a ContentView that first scans a QR code to extract a URL, displays the scanned URL, and provides a button to fetch data from that URL via an API call, showing the result or an error alert if the request fails. The interface initially shows a QR scanner overlay with the prompt «Scan QR code,» and upon successful scanning, it displays the URL and a button to trigger the API call, which asynchronously retrieves and displays the data or shows an error message in an alert if something goes wrong. The layout uses a vertical stack (VStack) to organize the UI elements and adjusts fonts and padding for better readability.

    QRCodeScannerView has to be implemented by using UIKit-UIViewControllerRepresentable bridge compontent:

    import SwiftUI
    import AVFoundation
    
    struct QRCodeScannerView: UIViewControllerRepresentable {
        class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
            var parent: QRCodeScannerView
    
            init(parent: QRCodeScannerView) {
                self.parent = parent
            }
    
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    parent.didFindCode(stringValue)
                }
            }
        }
    
        var didFindCode: (String) -> Void
    
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                return viewController
            }
    
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            Task {
                captureSession.startRunning()
            }
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    }

    The code creates a QR code scanner view that uses AVFoundation to capture and process QR codes, where it sets up a camera preview layer, configures a capture session to detect QR codes, and triggers a vibration and callback function (didFindCode) when a QR code is successfully scanned, passing the decoded string value to the parent view. The UIViewControllerRepresentable protocol bridges UIKit’s AVCaptureMetadataOutput functionality into SwiftUI, with a Coordinator class handling the metadata output delegation to process detected codes.

    Important point for scanning you need to deploy on a real iOS device:

    As soon as the QR code URL is read, it is presented to the user. When the user presses ‘Do API Call’, the iOS app performs a request to the service.

    Conclusions

    In this post I have presented how to consume API services where endpoints are could be dynamically provided by server via QR codes. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

  • Decluter Your Codebase: Periphery for Dead Code Detection

    Decluter Your Codebase: Periphery for Dead Code Detection

    Periphery addresses a common challenge faced by developers: maintaining clean, efficient codebases. Periphery, an open-source tool, offers a powerful solution for identifying and eliminating unused code in Swift projects, which can significantly improve app performance, reduce compile times, and enhance overall code quality.

    In this post we will explain how to setup Periphery on your iOS project for later on force any code situations to trgger expected warnings.

    Setup Periphery

    First step is install periphery by using homebrew installation method:

    brew install periphery

    Next step is creating juts a regular iOS Sample app with XCode: 

    brew install periphery

    Move to the folder where the project was created and type:

    And setup periphery for current iOS project:

    periphery scan --setup

    This will generate a hidden configuration file.

    Get back to XCode Project:

    And select the target, Build Phases, add (+) and NewScriptPhase. Do not forget unchecking ‘Based on dependency analysis’ option.

    Script basically consists in calling periphery for performing and scan. Press CMD+B for build and check if setup was ok:

    For fixing this issue just set User Script Sandboxing to No on Build Settings:

    Turn to build again for checking that now thi time all works fine:

    Detecting dead code

    Periphery aims to detect and report unused declarations in your code. These declarations include classes, structs, protocols, functions, properties, constructors, enums, typealiases, and associated types. As expected, Periphery can identify straightforward cases, such as a class that is no longer referenced anywhere in your codebase. For the purpose of this post we have added some dead code for checking that warnings are really trigger every time we build de code (CMD+B).

    Now appears new warnings that aims to dead code, but appart from thead code this utility also allows to detectct unused parameters:

    Conclusions

    Periphery is one of those tools that its setup effort is very low and will help you on removing dead code. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

    References

  • Beyond JSON Codables

    Beyond JSON Codables

    Explore decoding not just JSON but also CSV, XML, Plist, and YAML using Codable is interesting because developers often work with diverse data formats beyond JSON. While Codable is primarily designed for JSON, showcasing how to extend its functionality to handle other formats efficiently can help developers streamline their data parsing workflow.

    In this post, we will build an iOS sample app that will present how to parse codable data text format in JSON, CSV, XML, Plist and Yaml.

    JSON

    JSON (JavaScript Object Notation) is a lightweight data format used for storing and exchanging data in a human-readable and structured way. It is widely used because of its simplicity, readability, and compatibility with various programming languages. JSON represents data as key-value pairs, similar to a JavaScript object, making it easy to parse and generate. It is commonly used in web APIs, configurations, and data storage due to its efficiency, flexibility, and seamless integration with modern web technologies.

    Parsing code is implemented along with with its presentation:

    struct JSONView: View {
        @State private var people: [Person] = []
    
           func loadPeople() {
               let json = """
               [
                   {"name": "Juan", "age": 30},
                   {"name": "Ana", "age": 25},
                   {"name": "Carlos", "age": 35}
               ]
               """
               
               let data = Data(json.utf8)
               
               do {
                   let decodedPeople = try JSONDecoder().decode([Person].self, from: data)
                   self.people = decodedPeople
               } catch {
                   print("\(error)")
               }
    
           }
    
           var body: some View {
               NavigationView {
                   PeopleListView(people: people)
                   .navigationTitle("Persons List (JSON)")
               }
               .task {
                   loadPeople()
               }
           }
    }

    This SwiftUI code defines a JSONView struct that loads and displays a list of people from a hardcoded JSON string. The loadPeople() function decodes the JSON into an array of Person objects and assigns it to the @State variable people. The body property presents a NavigationView containing a PeopleListView, passing the people array to it. The .task modifier ensures loadPeople() runs asynchronously when the view appears, populating the list.

    XML

    XML (Extensible Markup Language) is a structured text format used to store and transport data in a human-readable and machine-readable way. It organizes data using custom tags that define elements hierarchically, making it widely used for data exchange between systems. XML is closely related to SOAP (Simple Object Access Protocol), as SOAP messages are formatted using XML. SOAP is a protocol for exchanging structured information in web services, relying on XML to define message structure, including headers and body content. This enables platform-independent communication between applications over protocols like HTTP and SMTP.

    XML is not supperted natively, so we have to import an SPM package such as SWXMLHash:

    Code for parsing and presenting:

    struct XMLView: View {
        @State private var people: [Person] = []
    
        func loadPeople() {
            let xmlString = """
                    <Persons>
                        <Person>
                            <Name>Teresa</Name>
                            <Age>35</Age>
                        </Person>
                        <Person>
                            <Name>Ana</Name>
                            <Age>45</Age>
                        </Person>
                        <Person>
                            <Name>Carlos</Name>
                            <Age>35</Age>
                        </Person>
                    </Persons>
                    """
    
            let xml = XMLHash.config { _ in }.parse(xmlString)
    
            do {
                let fetchedPeople: [Person] = try xml["Persons"].children.map { element in
                    let name: String = try element["Name"].value() ?? ""
                    let age: Int = try element["Age"].value() ?? -1
                    return Person(name: name, age: age)
                }
                people = fetchedPeople
            } catch {
                print("Error decoding XML: \(error)")
            }
        }
    
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                    .navigationTitle("Persons List (XML)")
            }
                .task {
                loadPeople()
            }
        }
    }

    The XMLView SwiftUI struct parses an XML string containing a list of people and displays them in a PeopleListView. It defines a @State variable people to store the parsed data and a loadPeople() function that uses the XMLHash library to extract names and ages from the XML. The parsed data is then stored in people, which updates the UI. The body consists of a NavigationView that displays PeopleListView, and loadPeople() is called asynchronously using .task {} when the view appears. This setup ensures that the list is populated dynamically from the XML data.

    CSV

    CSV (Comma-Separated Values) is a widely used data text file format because it is simple, lightweight, and universally compatible across different software and programming languages. It stores tabular data in plain text, making it easy to read, edit, and process without requiring specialized software. CSV files are also highly efficient for data exchange between applications, databases, and spreadsheets since they maintain a structured yet human-readable format. Additionally, their lack of complex metadata or formatting ensures broad support and ease of integration in data processing workflows.

    Parse in case of this file is are very simple string processing operations:

    struct CSVView: View {
        @State private var people: [Person] = []
        
        func loadPeople() {
            let csvString = """
                    name,age
                    Ricardo,40
                    Priscila,25
                    Carlos,35
                    """
    
            let lines = csvString.components(separatedBy: "\n")
                var persons: [Person] = []
    
                for line in lines.dropFirst() { // Remove header
                    let values = line.components(separatedBy: ",")
                    if values.count == 2, let age = Int(values[1]) {
                        persons.append(Person(name: values[0], age: age))
                    }
                }
            people = persons
        }
        
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                .navigationTitle("Persons List (CSV)")
            }
            .task {
                loadPeople()
            }
        }
    }

    The CSVView struct in SwiftUI loads a hardcoded CSV string containing names and ages, parses it into an array of Person objects, and displays them using PeopleListView. It first defines a @State variable people to store the parsed data. The loadPeople() function splits the CSV string into lines, ignores the header, extracts name and age values, converts them into Person objects, and updates people. The body contains a NavigationView that presents PeopleListView, and the .task modifier ensures loadPeople() runs when the view appears, allowing dynamic data population.

    Yaml

    YAML is a common data text file format because it is human-readable, easy to write, and supports complex data structures like lists and key-value mappings in a simple, indentation-based syntax. It is widely used for configuration files, data serialization, and automation scripts in DevOps, Kubernetes, and CI/CD pipelines due to its readability compared to JSON and XML. Additionally, YAML supports comments, anchors, and references, making it more flexible for structured data representation while remaining easy to integrate with various programming languages.

    Yaml, as well as XML, is also not supperted natively, so we have to import an SPM package such as Yams:

    Code for parsing and presenting:

    struct YamlView: View {
        @State private var people: [Person] = []
    
           func loadPeople() {
               let json = """
               - name: Sebastián
                 age: 32
               - name: Ana
                 age: 26
               - name: Pedro
                 age: 35
               """
               
               let data = Data(json.utf8)
               
               do {
                   let decodedPeople = try YAMLDecoder().decode([Person].self, from: data)
                   self.people = decodedPeople
               } catch {
                   print("\(error)")
               }
    
           }
    
           var body: some View {
               NavigationView {
                   PeopleListView(people: people)
                   .navigationTitle("Persons List (Yaml)")
               }
               .task {
                   loadPeople()
               }
           }
    }

    This SwiftUI view, YamlView, loads a list of people from a YAML-formatted string and displays them in a PeopleListView. It uses @State to store an array of Person objects and defines a loadPeople() function that converts a hardcoded YAML string into a Data object, decodes it into an array of Person structs using YAMLDecoder(), and updates the state. In the body, it presents a NavigationView with PeopleListView, setting «Persons List (Yaml)» as the navigation title. The .task modifier ensures loadPeople() runs asynchronously when the view appears.

    PList

    Last but not least, and old fatigue companiong that get along with us during many years in XCode projects.PLIST (Property List) is a common data text file format, especially in Apple’s ecosystem, because it is human-readable, structured, and easily parsed by both machines and developers. It supports hierarchical data storage, making it ideal for configuration files, preferences, and serialization in macOS and iOS applications. PLIST files can be formatted in XML or binary, allowing flexibility in readability and performance. Their native support in Apple frameworks, such as Core Foundation and Swift, makes them a default choice for storing structured data in a standardized way.

    A god point it that is supported natively:

    struct Plist: View {
        @State private var people: [Person] = []
    
        func loadPeople() {
            let plist = """
               <?xml version="1.0" encoding="UTF-8"?>
               <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
               <plist version="1.0">
                   <array>
                       <dict>
                           <key>name</key>
                           <string>Juan Pérez</string>
                           <key>age</key>
                           <integer>30</integer>
                       </dict>
                       <dict>
                           <key>name</key>
                           <string>Ana Gómez</string>
                           <key>age</key>
                           <integer>25</integer>
                       </dict>
                       <dict>
                           <key>name</key>
                           <string>Sílvia</string>
                           <key>age</key>
                           <integer>55</integer>
                       </dict>
                   </array>
               </plist>
               """
    
            let data = Data(plist.utf8)
    
            do {
                let decodedPeople = try PropertyListDecoder().decode([Person].self, from: data)
                self.people = decodedPeople
            } catch {
                print("\(error)")
            }
    
        }
    
        var body: some View {
            NavigationView {
                PeopleListView(people: people)
                    .navigationTitle("Persons List (Plist)")
            }
                .task {
                loadPeople()
            }
        }
    }

    This SwiftUI View named Plist loads a hardcoded Property List (Plist) containing an array of people, decodes it into an array of Person objects using PropertyListDecoder(), and displays the list using a PeopleListView. The loadPeople() function parses the embedded XML-based Plist data, converting it into Data, decodes it into an array of Person structs, and assigns it to the @State variable people. The body of the view contains a NavigationView that initializes PeopleListView with the decoded list and sets the navigation title. The .task modifier ensures that loadPeople() runs when the view appears, populating the list dynamically. If decoding fails, an error message is printed.

    Conclusions

    In this project, we have seen the most common data text formats and how to parse them. You can find source code used for writing this post in following repository