Etiqueta: Swift 6 migration

  • Seamless Keychain Data Migrations in iOS

    Seamless Keychain Data Migrations in iOS

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

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

    Keychain

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

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

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

    Sample App

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

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

    The structure that controls trial mechanism is following:

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

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

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

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

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

    review

    First change on Trial Data

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

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

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

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

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

    No more migration changes, no execute unit tests:

    Screenshot

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

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

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

    Next trial data

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

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

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

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

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

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

    Conclusions

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

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

    References

  • Enhancing iOS Apps with Redis

    Enhancing iOS Apps with Redis

    Connecting an iOS app to a Redis server can be highly beneficial, as it bridges the gap between mobile development and scalable backend solutions. Redis, known for its speed and efficiency, enhances performance by enabling fast caching, real-time data synchronization, and low-latency operations—critical for features like offline data storage, live notifications, and session management.

    In this post, we will build a Dockerized Node.js backend connected to a Dockerized Redis server. The client will be a simple iOS app that fetches a price list and updates the prices as well.

    Redis server

    The server is a sample Node.js application that will act as a backend facade. Let’s create a blank Node.js project:

    npm init -y

    To implement the server, we will need dotenv for loading environment variables from a .env file, express for setting up REST API endpoints, and ioredis for connecting to the Redis server.

    npm install dotenv express ioredis

    Node.Js server is following:

    require('dotenv').config(); // Fetch environment variables
    
    const express = require('express');
    const Redis = require('ioredis');
    
    const app = express();
    
    // Fetch environment variables
    const PORT = process.env.PORT || 3000;
    const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
    const REDIS_PORT = process.env.REDIS_PORT || 6379;
    
    // Connect with Redis by using environnment variables
    const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
    
    app.use(express.json());
    
    // 📌 Init some prices
    async function initializeSamplePrices() {
        const initialPrices = {
            "iphone": "999",
            "macbook": "1999",
            "ipad": "799",
            "airpods": "199"
        };
    
        for (const [product, price] of Object.entries(initialPrices)) {
            await redis.set(`price:${product}`, price);
        }
    
        console.log("✅ Prices initialized in Redis.");
    }
    
    // Initialize sample prices
    initializeSamplePrices();
    
    // 📌 Endpoint for getting a product price
    app.get('/price/:product', async (req, res) => {
        const price = await redis.get(`price:${req.params.product}`);
        
        if (price) {
            res.json({ product: req.params.product, price: price });
        } else {
            res.status(404).json({ error: "Product not found" });
        }
    });
    
    app.get('/prices', async (req, res) => {
        try {
            const keys = await redis.keys("price:*"); 
            
            // Fetch all prices
            const prices = await Promise.all(keys.map(async (key) => {
                const product = key.replace("price:", "");
                const price = await redis.get(key); 
                return { product: product, price: price };
            }));
    
            res.json(prices);
        } catch (error) {
            console.error("Error on getting prices:", error);
            res.status(500).json({ error: "Error on getting prices" });
        }
    });
    
    // 📌 Endpoint for adding or updating a product price
    app.post('/price', async (req, res) => {
        const { product, price } = req.body;
        if (!product || !price) {
            return res.status(400).json({ error: "Misssing data" });
        }
        
        await redis.set(`price:${product}`, price);
        res.json({ mensaje: "Price stored", product, price });
    });
    
    // 📌 Server listening on specied port
    app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
    

    This Node.js application sets up an Express server that interacts with a Redis database to store and retrieve product prices. It begins by loading environment variables using dotenv and establishes a connection to Redis with configurable host and port settings. When the server starts, it initializes Redis with some sample product prices (iPhone, MacBook, iPad, and AirPods). The Express app is configured to parse JSON requests, enabling it to handle API interactions effectively.

    The application provides multiple API endpoints: one for retrieving the price of a specific product (GET /price/:product), another for fetching all stored product prices (GET /prices), and a third for adding or updating a product price (POST /price). When a client requests a price, the server queries Redis, returning the value if found or a 404 error otherwise. The /prices endpoint retrieves all stored product prices by listing Redis keys and mapping them to their values. The /price POST endpoint allows clients to add or update prices by sending a JSON payload. Finally, the server listens on the configured port and logs a message indicating it is running.

    Sensitive intormateion is stored  in .env file:

    # Redis Configuration
    REDIS_HOST=redis
    REDIS_PORT=6379
    
    # Server Configuration
    PORT=3000

    Important: Please add .env to .gitignore to avoid compromising critical information in production environments. The next step is to define the Dockerfile for the Node.js server to ensure it is properly dockerized.

    FROM node:18
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["node", "server.js"]

    The Dockerfile sets up a containerized Node.js application. It begins with the official Node.js 18 image, sets the working directory to /app, and copies the package.json and package-lock.json files into the container. Then, it installs the dependencies using npm install. Afterward, it copies the rest of the application files into the container. The container exposes port 3000 and specifies node server.js as the command to run when the container starts, which typically launches the application server.

    To strictly copy only the necessary files into the container, create the following .dockerignore file:

    node_modules
    npm-debug.log
    .env
    .git
    

    The backend solution consists of two containerized servers. On one side, we have a Node.js backend facade that interacts with the iOS app. On the other side, the backend relies on cache storage in a Redis server.

    services:
      redis:
        image: redis:latest
        container_name: redis-server
        ports:
          - "6379:6379"
        restart: always
    
      backend:
        build: .
        container_name: node-backend
        ports:
          - "${PORT}:${PORT}"
        depends_on:
          - redis
        env_file:
          - .env

    This code defines a docker-compose.yml configuration file for setting up two services: a Redis server and a backend Node.js application. The Redis service uses the latest Redis image, exposing port 6379 and ensuring the container always restarts. The backend service is built from the current directory (.) using a Dockerfile, exposes a dynamic port based on the environment variable ${PORT}, and depends on the Redis service. Additionally, the backend container loads environment variables from a .env file. The depends_on directive ensures Redis starts before the backend service.

    Build image and deploy containers:

    docker-compose up --build -d

    Make sure both containers are running by using docker ps. Finally, type the following URL in any browser to fetch the prices: http://localhost:3000/prices.

    Backend side is ready!

    iOS Sample App

    The iOS sample app fetches product prices and allows users to update any price.

    Core app functionallity is following:

    import Foundation
    
    struct Product: Codable, Identifiable, Sendable {
        var id: String { product }
        let product: String
        var price: String
    }
    
    @MainActor
    class PriceService: ObservableObject {
        @Published var products: [Product] = []
    
        func fetchPrices() async {
            guard let url = URL(string: "http://localhost:3000/prices") else { return }
    
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                let decoder = JSONDecoder()
                if let productos = try? decoder.decode([Product].self, from: data) {
                    self.products = productos
                }
            } catch {
                print("Error fetching prices:", error)
            }
        }
    
        func updatePrice(product: String, price: String) async {
            guard let url = URL(string: "http://localhost:3000/price") else { return }
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
            let body: [String: String] = ["product": product, "price": price]
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
    
            do {
                let (_, _) = try await URLSession.shared.data(for: request)
                await self.fetchPrices()
            } catch {
                print("Error updating price: \(error.localizedDescription)")
            }
        }
    }

    This Swift code defines a model and a service for fetching and updating product prices. The Product struct represents a product with an ID, name, and price, conforming to the Codable, Identifiable, and Sendable protocols. The PriceService class is an observable object that manages a list of products and provides methods for interacting with a remote server.

    The fetchPrices() function asynchronously fetches product prices from a specified URL (http://localhost:3000/prices) using URLSession, decodes the returned JSON into an array of Product objects, and updates the products array. The updatePrice(product:price:) function sends a POST request to update the price of a specific product by sending a JSON payload to http://localhost:3000/price. After a successful update, it calls fetchPrices() again to refresh the list of products. Both functions handle errors by printing error messages if the network request fails.

    In this review, we update the price using the app and then return to the browser to verify that the price update has been reflected in the Redis server.

    Conclusions

    In this project, we demonstrated how easy it is to set up a sample backend server using Redis and interact with it through an iOS app as the client.

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

    References

  • Testing an iOS Location Manager

    Testing an iOS Location Manager

    This post explains how to validate hardware-dependent components like the LocationManager, which relies on GPS hardware. Testing such managers, including LocationManager and VideoManager, is crucial for addressing challenges developers face, such as hardware constraints, environmental variability, and simulator limitations. By mastering these techniques, you can ensure robust and reliable application behavior in real-world scenarios.

    I will guide you through the process of validating a LocationManager, introduce its test support structures, and provide examples of unit tests. Along the way, we’ll explore key techniques like mocking system services, dependency injection, and efficient testing strategies for simulators and real devices.

    This improved version enhances clarity, reduces redundancy, and improves flow while retaining all the critical details. Let me know if you’d like further refinements!

    Location Manager

    In this case, we have a location manager to handle geographic data efficiently and ensure accurate location tracking.

    import Foundation
    import CoreLocation
    
    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    @GlobalManager
    class LocationManager: NSObject, ObservableObject  {
        private var clLocationManager: CLLocationManager? = nil
    
        @MainActor
        @Published var permissionGranted: Bool = false
        private var internalPermissionGranted: Bool = false {
             didSet {
                Task { [internalPermissionGranted] in
                    await MainActor.run {
                        self.permissionGranted = internalPermissionGranted
                    }
                }
            }
        }
        
        @MainActor
        @Published var speed: Double = 0.0
        private var internalSpeed: Double = 0.0 {
             didSet {
                Task { [internalSpeed] in
                    await MainActor.run {
                        self.speed = internalSpeed
                    }
                }
            }
        }
        
        init(clLocationManager: CLLocationManager = CLLocationManager()) {
            super.init()
            self.clLocationManager = clLocationManager
            clLocationManager.delegate = self
        }
        
        func checkPermission() {
            clLocationManager?.requestWhenInUseAuthorization()
        }
    }
    
    extension LocationManager: @preconcurrency CLLocationManagerDelegate {
        
        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
            let statuses: [CLAuthorizationStatus] = [.authorizedWhenInUse, .authorizedAlways]
            if statuses.contains(status) {
                internalPermissionGranted = true
                Task {
                    internalStartUpdatingLocation()
                }
            } else if status == .notDetermined {
                checkPermission()
            } else {
                internalPermissionGranted = false
            }
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
            internalSpeed = location.speed
        }
        
        private func internalStartUpdatingLocation() {
            guard CLLocationManager.locationServicesEnabled() else { return }
            clLocationManager?.startUpdatingLocation()
        }
    }
    
    This Swift code defines a LocationManager class that manages location permissions and tracking, integrating with SwiftUI’s reactive model. It uses CLLocationManager to handle location updates and authorization, updating @Published properties like permissionGranted and speed for UI binding. The class leverages Swift’s concurrency features, including @MainActor and @globalActor, to ensure thread-safe updates to the UI on the main thread. Private properties (internalPermissionGranted and internalSpeed) encapsulate internal state, while public @Published properties notify views of changes. By conforming to CLLocationManagerDelegate, it handles permission requests, starts location updates, and updates speed in response to location changes, ensuring a clean, reactive, and thread-safe integration with SwiftUI.

    Location Manager

    The key is to mock CLLocationManager and override its methods to suit the needs of your tests:

    class LocationManagerMock: CLLocationManager {
        var clAuthorizationStatus: CLAuthorizationStatus = .notDetermined
        
        override func requestWhenInUseAuthorization() {
            delegate?.locationManager!(self, didChangeAuthorization: clAuthorizationStatus)
        }
        
        override func startUpdatingLocation() {
            let sampleLocation = CLLocation(
                coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
                altitude: 10.0,
                horizontalAccuracy: 5.0,
                verticalAccuracy: 5.0,
                course: 90.0,
                speed: 10.0,
                timestamp: Date()
            )
            delegate?.locationManager!(self, didUpdateLocations: [sampleLocation])
        }
    }

    For our test purposes, we are validating the location-granted request service and starting the location update process. During permission validation, we use an attribute to provide the desired response when requestWhenInUseAuthorization is executed. Additionally, we include a sample CLLocation to simulate the location data when startUpdatingLocation is called.

    To ensure robust validation of authorization, we have implemented the following unit tests:

        @Test func testAthorizacionRequestDenied() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus = .denied
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 1_000_000)
            await #expect(sut.permissionGranted == false)
        }
    
    
        @Test func testAthorizacionRequestAuthorized() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus =  .authorizedWhenInUse
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 1_000_000)
            await #expect(sut.permissionGranted == true)
        }
    Validates scenarios where the user grants or denies location services authorization. Also validates location updates.
        @Test func testStartUpdatingLocation() async throws {
            let locationManagerMock = LocationManagerMock()
            locationManagerMock.clAuthorizationStatus =  .authorizedWhenInUse
            let sut = await LocationManager(clLocationManager: locationManagerMock)
            await sut.checkPermission()
            // Wait for the @Published speed property to update
            try await Task.sleep(nanoseconds: 50_000_000)
                   
            await #expect(sut.speed == 10.00)
        }

    Basically we check location speed.

    Conclusions

    In this post, I have presented a method for validating hardware-dependent issues, such as GPS information. You can find the source code used for this post in the repository linked below.

  • Agnostic Swift Data

    Agnostic Swift Data

    One of the most shocking experiences I encountered as an iOS developer was working with Core Data, now known as Swift Data. While there are many architectural patterns for software development, I have rarely seen an approach that combines view logic with database access code. Separating data access from the rest of the application has several advantages: it centralizes all data operations through a single access point, facilitates better testing, and ensures that changes to the data access API do not impact the rest of the codebase—only the data access layer needs adaptation.

    Additionally, most applications I’ve worked on require some level of data processing before presenting information to users. While Apple excels in many areas, their examples of how to use Core Data or Swift Data do not align well with my daily development needs. This is why I decided to write a post demonstrating how to reorganize some components to better suit these requirements.

    In this post, we will refactor a standard Swift Data implementation by decoupling Swift Data from the View components.

    Custom Swift Data

    The Starting Point sample app is an application that manages a persisted task list using Swift Data.
    import SwiftUI
    import SwiftData
    
    struct TaskListView: View {
        @Environment(\.modelContext) private var modelContext
        @Query var tasks: [TaskDB]
    
        @State private var showAddTaskView = false
    
        var body: some View {
            NavigationView {
                List {
                    ForEach(tasks) { task in
                        HStack {
                            Text(task.title)
                                .strikethrough(task.isCompleted, color: .gray)
                            Spacer()
                            Button(action: {
                                task.isCompleted.toggle()
                                try? modelContext.save()
                            }) {
                                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                            }
                            .buttonStyle(BorderlessButtonStyle())
                        }
                    }
                    .onDelete(perform: deleteTasks)
                }
                .navigationTitle("Tasks")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: { showAddTaskView = true }) {
                            Image(systemName: "plus")
                        }
                    }
                }
                .sheet(isPresented: $showAddTaskView) {
                    AddTaskView()
                }
            }
        }
    
        private func deleteTasks(at offsets: IndexSet) {
            for index in offsets {
                modelContext.delete(tasks[index])
            }
            try? modelContext.save()
        }
    }

    As observed in the code above, the view code is intertwined with data access logic (Swift Data). While the code is functioning correctly, there are several concerns:

    1. Framework Dependency: If the framework changes, will all the views using this framework need to be updated?
    2. Unit Testing: Are there existing unit tests to validate CRUD operations on the database?
    3. Debugging Complexity: If I need to debug when a record is added, do I need to set breakpoints across all views to identify which one is performing this task?
    4. Code Organization: Is database-related logic spread across multiple project views?

    Due to these reasons, I have decided to refactor the code.

    Refactoring the app

    This is a sample app, so I will not perform a strict refactor. Instead, I will duplicate the views to include both approaches within the same app, allowing cross-CRUD operations between the two views.

    @main
    struct AgnosticSwiftDataApp: App {
        var body: some Scene {
            WindowGroup {
                TabView {
                    TaskListView()
                        .tabItem {
                        Label("SwiftData", systemImage: "list.dash")
                    }
                        .modelContainer(for: [TaskDB.self])
                    AgnosticTaskListView()
                        .tabItem {
                        Label("Agnostic", systemImage: "list.dash")
                    }
                }
            }
        }
    }
    

    First of all we are going to create a component that handles all DB operations:

    import SwiftData
    import Foundation
    
    @MainActor
    protocol DBManagerProtocol {
        func addTask(_ task: Task)
        func updateTask(_ task: Task)
        func removeTask(_ task: Task)
        func fetchTasks() -> [Task]
    }
    
    @MainActor
    class DBManager: NSObject, ObservableObject {
    
        @Published var tasks: [Task] = []
    
    
        static let shared = DBManager()
    
        var modelContainer: ModelContainer? = nil
    
        var modelContext: ModelContext? {
            modelContainer?.mainContext
        }
    
        private init(isStoredInMemoryOnly: Bool = false) {
            let configurations = ModelConfiguration(isStoredInMemoryOnly: isStoredInMemoryOnly)
            do {
                modelContainer = try ModelContainer(for: TaskDB.self, configurations: configurations)
            } catch {
                fatalError("Failed to initialize ModelContainer: \(error)")
            }
        }
    }
    
    extension DBManager: DBManagerProtocol {
    
        func removeTask(_ task: Task) {
            guard let modelContext,
                let taskDB = fetchTask(by: task.id) else { return }
    
            modelContext.delete(taskDB)
    
            do {
                try modelContext.save()
            } catch {
                print("Error on deleting task: \(error)")
            }
        }
    
        func updateTask(_ task: Task) {
            guard let modelContext,
                let taskDB = fetchTask(by: task.id) else { return }
    
            taskDB.title = task.title
            taskDB.isCompleted = task.isCompleted
    
            do {
                try modelContext.save()
            } catch {
                print("Error on updating task: \(error)")
            }
            return
        }
    
        private func fetchTask(by id: UUID) -> TaskDB? {
            guard let modelContext else { return nil }
    
            let predicate = #Predicate<TaskDB> { task in
                task.id == id
            }
    
            let descriptor = FetchDescriptor<TaskDB>(predicate: predicate)
    
            do {
                let tasks = try modelContext.fetch(descriptor)
                return tasks.first
            } catch {
                print("Error fetching task: \(error)")
                return nil
            }
        }
    
        func addTask(_ task: Task) {
            guard let modelContext else { return }
            let taskDB = task.toTaskDB()
            modelContext.insert(taskDB)
            do {
                try modelContext.save()
                tasks = fetchTasks()
            } catch {
                print("Error addig tasks: \(error.localizedDescription)")
            }
        }
    
        func fetchTasks() -> [Task] {
            guard let modelContext else { return [] }
    
            let fetchRequest = FetchDescriptor<TaskDB>()
    
            do {
                let tasksDB = try modelContext.fetch(fetchRequest)
                tasks = tasksDB.map { .init(taskDB: $0) }
                return tasks 
            } catch {
                print("Error fetching tasks: \(error.localizedDescription)")
                return []
            }
        }
    
        func deleteAllData() {
            guard let modelContext else { return }
            do {
                try modelContext.delete(model: TaskDB.self)
            } catch {
                print("Error on removing all data: \(error)")
            }
            tasks = fetchTasks()
        }
    }
    

    In a single, dedicated file, all database operations are centralized. This approach offers several benefits:

    • If the framework changes, only the function responsible for performing the database operations needs to be updated.
    • Debugging is simplified. To track when a database operation occurs, you only need to set a single breakpoint in the corresponding function.
    • Unit testing is more effective. Each database operation can now be tested in isolation.
    import Foundation
    import Testing
    import SwiftData
    @testable import AgnosticSwiftData
    
    extension DBManager {
        func setMemoryStorage(isStoredInMemoryOnly: Bool) {
            let configurations = ModelConfiguration(isStoredInMemoryOnly: isStoredInMemoryOnly)
            do {
                modelContainer = try ModelContainer(for: TaskDB.self, configurations: configurations)
            } catch {
                fatalError("Failed to initialize ModelContainer: \(error)")
            }
        }
    }
    
    @Suite("DBManagerTests", .serialized)
    struct DBManagerTests {
        
        func getSUT() async throws -> DBManager {
            let dbManager = await DBManager.shared
            await dbManager.setMemoryStorage(isStoredInMemoryOnly: true)
            await dbManager.deleteAllData()
            return dbManager
        }
        
        @Test("Add Task")
        func testAddTask() async throws {
            let dbManager = try await getSUT()
            let task = Task(id: UUID(), title: "Test Task", isCompleted: false)
            
            await dbManager.addTask(task)
            
            let fetchedTasks = await dbManager.fetchTasks()
            #expect(fetchedTasks.count == 1)
            #expect(fetchedTasks.first?.title == "Test Task")
            
            await #expect(dbManager.tasks.count == 1)
            await #expect(dbManager.tasks[0].title == "Test Task")
            await #expect(dbManager.tasks[0].isCompleted == false)
        }
        
        @Test("Update Task")
        func testUpateTask() async throws {
            let dbManager = try await getSUT()
            let task = Task(id: UUID(), title: "Test Task", isCompleted: false)
            await dbManager.addTask(task)
            
            let newTask = Task(id: task.id, title: "Updated Task", isCompleted: true)
            await dbManager.updateTask(newTask)
            
            let fetchedTasks = await dbManager.fetchTasks()
            #expect(fetchedTasks.count == 1)
            #expect(fetchedTasks.first?.title == "Updated Task")
            #expect(fetchedTasks.first?.isCompleted == true)
            
            await #expect(dbManager.tasks.count == 1)
            await #expect(dbManager.tasks[0].title == "Updated Task")
            await #expect(dbManager.tasks[0].isCompleted == true)
        }
        
        @Test("Delete Task")
        func testDeleteTask() async throws {
            let dbManager = try await getSUT()
            let task = Task(id: UUID(), title: "Test Task", isCompleted: false)
            await dbManager.addTask(task)
            
            await dbManager.removeTask(task)
            
            let fetchedTasks = await dbManager.fetchTasks()
            #expect(fetchedTasks.isEmpty)
            
            await #expect(dbManager.tasks.isEmpty)
        }
        
        
    
        @Test("Fetch Tasks")
        func testFetchTasks() async throws {
            let dbManager = try await getSUT()
            let task1 = Task(id: UUID(), title: "Task 1", isCompleted: false)
            let task2 = Task(id: UUID(), title: "Task 2", isCompleted: true)
            
            await dbManager.addTask(task1)
            await dbManager.addTask(task2)
            
            let fetchedTasks = await dbManager.fetchTasks()
            #expect(fetchedTasks.count == 2)
            #expect(fetchedTasks.contains { $0.title == "Task 1" })
            #expect(fetchedTasks.contains { $0.title == "Task 2" })
            
            await #expect(dbManager.tasks.count == 2)
            await #expect(dbManager.tasks[0].title == "Task 1")
            await #expect(dbManager.tasks[0].isCompleted == false)
            await #expect(dbManager.tasks[1].title == "Task 2")
            await #expect(dbManager.tasks[1].isCompleted == true)
        }
    
        @Test("Delete All Data")
        func testDeleteAllData() async throws {
            let dbManager = try await getSUT()
            let task = Task(id: UUID(), title: "Test Task", isCompleted: false)
            
            await dbManager.addTask(task)
            await dbManager.deleteAllData()
            
            let fetchedTasks = await dbManager.fetchTasks()
            #expect(fetchedTasks.isEmpty)
            
            await #expect(dbManager.tasks.isEmpty)
        }
        
        @Test("Model Context Nil")
        @MainActor
        func testModelContextNil() async throws {
            let dbManager = try await getSUT()
            dbManager.modelContainer = nil
            
            dbManager.addTask(Task(id: UUID(), title: "Test", isCompleted: false))
            #expect(try dbManager.fetchTasks().isEmpty)
            
            #expect(dbManager.tasks.count == 0)
        }
        
    }
    

    And this is the view:

    import SwiftUI
    
    struct AgnosticTaskListView: View {
        @StateObject private var viewModel: AgnosticTaskLlistViewModel = .init()
        
        @State private var showAddTaskView = false
        
        var body: some View {
            NavigationView {
                List {
                    ForEach(viewModel.tasks) { task in
                        HStack {
                            Text(task.title)
                                .strikethrough(task.isCompleted, color: .gray)
                            Spacer()
                            Button(action: {
                                viewModel.toogleTask(task: task)
                            }) {
                                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                            }
                            .buttonStyle(BorderlessButtonStyle())
                        }
                    }
                    .onDelete(perform: deleteTasks)
                }
                .navigationTitle("Tasks")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button(action: { showAddTaskView = true }) {
                            Image(systemName: "plus")
                        }
                    }
                }
                .sheet(isPresented: $showAddTaskView) {
                    AddTaskViewA()
                        .environmentObject(viewModel)
                }
            }.onAppear {
                viewModel.fetchTasks()
            }
        }
        
        private func deleteTasks(at offsets: IndexSet) {
            viewModel.removeTask(at: offsets)
        }
    }

    SwiftData is not imported, nor is the SwiftData API used in the view. To refactor the view, I adopted the MVVM approach. Here is the ViewModel:

    import Foundation
    
    @MainActor
    protocol AgnosticTaskLlistViewModelProtocol {
        func addTask(title: String)
        func removeTask(at offsets: IndexSet)
        func toogleTask(task: Task)
    }
    
    @MainActor
    final class AgnosticTaskLlistViewModel: ObservableObject {
        @Published var tasks: [Task] = []
        
        let dbManager = appSingletons.dbManager
        
        init() {
            dbManager.$tasks.assign(to: &$tasks)
        }
    }
    
    extension AgnosticTaskLlistViewModel: AgnosticTaskLlistViewModelProtocol {
        func addTask(title: String) {
            let task = Task(title: title)
            dbManager.addTask(task)
        }
        
        func removeTask(at offsets: IndexSet) {
            for index in offsets {
                dbManager.removeTask(tasks[index])
            }
        }
        
        func toogleTask(task: Task) {
            let task = Task(id: task.id, title: task.title, isCompleted: !task.isCompleted)
            dbManager.updateTask(task)
        }
        
        func fetchTasks() {
            _ = dbManager.fetchTasks()
        }
    }

    The ViewModel facilitates database operations to be consumed by the view. It includes a tasks list, a published attribute that is directly linked to the @Published DBManager.tasks attribute.

    Finally the resulting sample project looks like:

    Conclusions

    In this post, I present an alternative approach to handling databases with Swift Data, different from what Apple offers. Let me clarify: Apple creates excellent tools, but this framework does not fully meet my day-to-day requirements for local data persistence in a database.

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

    References

  • watchOS App for Health Monitoring Essentials

    watchOS App for Health Monitoring Essentials

    Creating a WatchOS app that initiates workout sessions and retrieves heart rate and calories burned is an exciting opportunity to bridge the gap between health tech and software development. With the growing interest in wearable technology and fitness tracking, such a guide provides practical value by teaching developers how to leverage WatchOS-specific APIs like HealthKit and WorkoutKit. It offers a hands-on project that appeals to diverse audiences, from aspiring developers to fitness entrepreneurs, while showcasing real-world applications and fostering innovation in health monitoring. By sharing this knowledge, you not only empower readers to build functional apps but also inspire them to explore new possibilities in the intersection of technology and wellness

    Blank watchOS app

    We are going to create a blank, ready-to-deploy watchOS app. A very essential point is to have access to an Apple Watch for validating the steps explained in this post. In my case, I am using an Apple Watch Series 9. Now, open Xcode and create a new blank project.

    And select ‘watchOS’ and ‘App’. To avoid overloading the sample project with unnecessary elements for the purpose of this post, choose ‘Watch-only App’.
    Add signing capabilities for using Healthkit.

    The CaloriesBurner target serves as the main wrapper for your project, enabling you to submit it to the App Store. The CaloriesBurner Watch App target is specifically designed for building the watchOS app. This app bundle includes the watchOS app’s code and assets.

    Finally, navigate to the Build Settings for the CaloriesBurner Watch App target and set the HealthShareUsageDescription and HealthUpdateUsageDescription fields with appropriate description messages.

    We must not forget to prepare the app for Swift 6.0. Set ‘Swift Concurrency Checking’ to ‘Complete’.

    … and set Swift Language Version.

    Before proceeding with deployment and verifying that the app is displayed properly on a real Apple Watch device, ensure that development mode is enabled on the device.

    Request authorization

    HealthKit requires explicit user consent to access or share specific types of health information, ensuring users have full control over their data and its usage. It requests user permission to read data such as heart rate and active energy burned, and to write workout data to HealthKit.

        func requestAuthorization() async {
            let typesToShare: Set = [HKObjectType.workoutType()]
            let typesToRead: Set = [HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!]
    
            do {
                try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)
                internalWorkoutSessionState = .notStarted
            } catch {
                internalWorkoutSessionState = .needsAuthorization
            }
        }

    If authorization succeeds, the internal state (internalWorkoutSessionState) is updated to .notStarted, indicating readiness for a workout session.

    Ensure that requestAuthorization is called when the view is presented, but only once.

    struct ContentView: View {
        @StateObject var healthkitManager = appSingletons.healthkitManager
        var body: some View {
            VStack {
               ...
            }
            .padding()
            .task {
                Task {
                    await healthkitManager.requestAuthorization()
                }
            }
        }
    }

    Build and deploy…

    On starting the app, watchOS will request your permission to access the app, your heart rate, and calories burned during workouts.

    Workout sample application

    Once the app is properly configured and granted all the necessary permissions to request health data, it is ready to start a workout session. To initiate a session, the app provides a button labeled «Start». When the user presses this button, the session begins, displaying the user’s heart rate and tracking the calories burned in real time.

    When the user presses the Start button, the HealthkitManager.startWorkoutSession method is called:

        func startWorkoutSession() async {
            guard session == nil, timer == nil else { return }
    
            guard HKHealthStore.isHealthDataAvailable() else {
                print("HealthKit is not ready on this device")
                return
            }
    
            let configuration = HKWorkoutConfiguration()
            configuration.activityType = .running
            configuration.locationType = .outdoor
    
            do {
                session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
                session?.delegate = self
    
                builder = session?.associatedWorkoutBuilder()
                builder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration)
                builder?.delegate = self
                session?.startActivity(with: Date())
    
                do {
                    try await builder?.beginCollection(at: Date())
                } catch {
                    print("Error starting workout collection: \(error.localizedDescription)")
                    session?.end()
                    internalWorkoutSessionState = .needsAuthorization
                }
    
                internalWorkoutSessionState = .started
            } catch {
                print("Error creating session or builder: \(error.localizedDescription)")
                session = nil
            }
        }

    To retrieve the heart rate and calories burned, the HealthKitManager must implement the HKLiveWorkoutBuilderDelegate protocol.

    extension HealthkitManager: HKLiveWorkoutBuilderDelegate {
        nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
            print("Workout event collected.")
        }
    
        func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf types: Set<HKSampleType>) {
            for type in types {
                if let quantityType = type as? HKQuantityType, quantityType == HKQuantityType.quantityType(forIdentifier: .heartRate) {
                    handleHeartRateData(from: workoutBuilder)
                }
                if let quantityType = type as? HKQuantityType, quantityType == HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
                    handleActiveEnergyData(from: workoutBuilder)
                }
            }
        }
    
        private func handleHeartRateData(from builder: HKLiveWorkoutBuilder) {
            if let statistics = builder.statistics(for: HKQuantityType.quantityType(forIdentifier: .heartRate)!) {
                let heartRateUnit = HKUnit(from: "count/min")
                if let heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) {
                    print("Heart rate: \(heartRate) BPM")
                    internalHeartRate = "\(heartRate) BPM"
                }
            }
        }
    
        private func handleActiveEnergyData(from builder: HKLiveWorkoutBuilder) {
            if let statistics = builder.statistics(for: HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!) {
                let energyUnit = HKUnit.kilocalorie()
                if let activeEnergy = statistics.sumQuantity()?.doubleValue(for: energyUnit) {
                    print("Active Energy Burned: \(activeEnergy) kcal")
                    internalCaloriesBurned = String(format: "%.2f kcal", activeEnergy)
                }
            }
        }
    }

    This code defines a HealthkitManager that integrates with Apple HealthKit to track live workout data, specifically heart rate and active energy burned during a workout. It uses the HKLiveWorkoutBuilderDelegate to monitor real-time workout events and data collection. The delegate method workoutBuilder(_:didCollectDataOf:) processes types of health data, focusing on heart rate and active energy burned, which are handled by respective private methods (handleHeartRateData and handleActiveEnergyData). These methods retrieve and print the latest values from HealthKit and store them internally. A workout session is configured for running (outdoor) using HKWorkoutConfiguration, and a live workout builder is initialized to collect data. The session and builder are started with error handling for initialization and data collection failures, and internal states are updated to track the workout’s progress. This setup enables live monitoring and analysis of health metrics during a workout.

    For stoping workout session responsible code is folloing:

        func stopWorkoutSession() async {
            guard let session else { return }
            session.end()
            do {
                try await builder?.endCollection(at: Date())
            } catch {
                print("Error on ending data collection: \(error.localizedDescription)")
            }
            do {
                try await builder?.finishWorkout()
            } catch {
                print("Error on ending training: \(error.localizedDescription)")
            }
    
            internalWorkoutSessionState = .ended
        }

    The stopWorkoutSession function is a method that terminates a workout session by first ensuring the session object is non-nil and calling its end method. It then attempts to asynchronously stop data collection (endCollection) and finish the workout (finishWorkout) using the optional builder object, handling any errors in do-catch blocks to log failures without interrupting execution. Finally, it updates the internal state (internalWorkoutSessionState) to .ended, ensuring the session is marked as concluded within the app’s logic. This function manages state, error handling, and asynchronous operations crucial for gracefully ending a workout session in a fitness tracking app.

    To see all of this in action, I have prepared the following video:

    After requesting permission for having access to health data and press start button, heart beat is presented along with the calories burned since user pressed the button.

    Conclusions

    In this post, I have demonstrated how to set up a watchOS app and configure HealthKit to display heart rate and calories burned. You can find the source code used for this post in the repository linked below.

    References

  • Crafting a Simple iOS App Using GraphQL APIs

    Crafting a Simple iOS App Using GraphQL APIs

    Using GraphQL instead of REST offers greater flexibility and efficiency. It allows clients to request precisely the data they need through a single endpoint, avoiding issues like over-fetching or under-fetching. Its strongly-typed schema enhances the developer experience by providing built-in documentation and easy introspection. Additionally, GraphQL’s real-time capabilities, enabled through subscriptions, support features such as live updates. It also excels at aggregating data from multiple sources into a unified API, making it an excellent choice for complex systems. However, it can introduce added server-side complexity and may not be necessary for simple or static applications where REST is sufficient.

    In this post, we will create a minimal, dockerized GraphQL server and implement an iOS client app that performs a request. At the end of the post, you will find a link to a GitHub repository containing the source code for further review.

    Setup a graphQL Server

    In this section, we will develop a minimal GraphQL dockerized server. The purpose of this post is not to dive deeply into GraphQL or Docker. However, I recommend spending some time exploring tutorials on these topics. At the end of the post, you will find links to the tutorials I followed.

    The server code fetches data from hardcoded sources for simplicity. In a typical scenario, the data would be retrieved from a database or other data source:

    import { ApolloServer, gql } from 'apollo-server';
    
    // Sample data
    const users = [
        { id: '1', name: 'Brandon Flowers', email: 'brandon.flowers@example.com' },
        { id: '2', name: 'Dave Keuning', email: 'dave.keuning@example.com' },
        { id: '3', name: 'Ronnie Vannucci Jr.', email: 'ronnie.vannuccijr@example.com' },
        { id: '4', name: 'Mark Stoermer', email: 'mark.stoermer@example.com' },
      ];
    
    // Schema
    const typeDefs = gql`
      type Query {
        getUser(id: ID!): User
      }
    
      type User {
        id: ID!
        name: String!
        email: String!
      }
    `;
    
    // Resolver
    const resolvers = {
      Query: {
        getUser: (_, { id }) => {
          const user =  users.find(user => user.id === id);
          if (!user) {
            throw new Error(`User with ID ${id} not found`);
          }
          return user;
        },
      },
    };
    
    // Setup server
    const server = new ApolloServer({ typeDefs, resolvers });
    
    // Start up server
    server.listen().then(({ url }) => {
      console.log(`🚀 Servidor listo en ${url}`);
    });
    The server is containerized using Docker, eliminating the need to install npm on your local machine. It will be deployed within a Linux-based image preconfigured with Node.js:
    # Usamos una imagen oficial de Node.js
    FROM node:18
    
    # Establecemos el directorio de trabajo
    WORKDIR /usr/src/app
    
    # Copiamos los archivos del proyecto a la imagen
    COPY . .
    
    # Instalamos las dependencias del proyecto
    RUN npm install
    
    # Exponemos el puerto 4000
    EXPOSE 4000
    
    # Ejecutamos el servidor
    CMD ["node", "server.js"]

    This Dockerfile packages a Node.js application into a container. When the container is run, it performs the following actions:

    1. Sets up the application directory.
    2. Installs the required dependencies.
    3. Starts the Node.js server located in server.js, making the application accessible on port 4000.

    To build the Docker image, use the following command:

    docker build -t graphql-server .
    Once the image is built, simply run the container image
    docker run -p 4000:4000 graphql-server
    Type ‘http://localhost:4000/’ URL on your favourite browser:
    The GraphQL server is now online. To start querying the server, simply click ‘Query your server,’ and the Sandbox will open for you to begin querying. The sample query that we will execute is as follows:
    query  {
      getUser(id: "4") {
        id
        name
        email
      }
    }
    Up to this point, the server is ready to handle requests. In the next section, we will develop an iOS sample app client.

    Sample iOS graphQL client app

    For the sample iOS GraphQL client app, we will follow the MVVM architecture. The app will use Swift 6 and have Strict Concurrency Checking enabled. The app’s usage is as follows:

    The user enters an ID (from 1 to 4), and the app prompts for the user’s name. The server then responds with the name associated with that ID. I will skip the view and view model components, as there is nothing new to discuss there. However, if you’re interested, you can find a link to the GitHub repository.

    The key aspect of the implementation lies in the GraphQLManager, which is responsible for fetching GraphQL data. Instead of using a GraphQL SPM component like Apollo-iOS, I chose to implement the data fetching using URLSession. This decision was made to avoid introducing a third-party dependency. At this level, the code remains simple, and I will not expand further on this in the post.

    Regarding Swift 6 compliance, the code is executed within a @GlobalActor to avoid overloading the @MainActor.

    import SwiftUI
    import Foundation
    
    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    @GlobalManager
    protocol GraphQLManagerProtocol {
        func fetchData(userId: String) async -> (Result<User, Error>)
    }
    
    @GlobalManager
    class GraphQLManager: ObservableObject {
    
        @MainActor
        static let shared = GraphQLManager()
    
    }
    
    extension GraphQLManager: GraphQLManagerProtocol {
    
        func fetchData(userId: String) async -> (Result<User, Error>) {
            
            let url = URL(string: "http://localhost:4000/")!
            let query = """
            query  {
              getUser(id: "\(userId)") {
                id
                name
              }
            }
            """
            
            let body: [String: Any] = [
                "query": query
            ]
            guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
                return .failure(NSError(domain: "Invalid JSON", code: 400, userInfo: nil))
            }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = jsonData
            
            do {
                let (data, response) = try await URLSession.shared.data(for: request)
                guard let httpResponse = response as? HTTPURLResponse,
                    (200...299).contains(httpResponse.statusCode) else {
                    return .failure(ErrorService.invalidHTTPResponse)
                }
                do {
                    let graphQLResponse = try JSONDecoder().decode(GraphQLResponse<GraphQLQuery>.self, from: data)
                    return .success(graphQLResponse.data.user)
                } catch {
                    return .failure(ErrorService.failedOnParsingJSON)
                }
            } catch {
                return .failure(ErrorService.errorResponse(error))
            }
        }
      
    }

    Conclusions

    GraphQL is another alternative for implementing client-server requests. It does not differ significantly from the REST approach. You can find source code used for writing this post in following repository.

    References

  • Harnessing NFC Technology in your iOS App

    Harnessing NFC Technology in your iOS App

    Near Field Communication (NFC) is a short-range wireless technology that enables communication between two compatible devices when brought within a few centimeters of each other. This technology powers various applications, including contactless payments, data sharing, and access control, offering faster and more convenient transactions. NFC’s ease of use eliminates the need for complex pairing processes, enabling seamless interactions between devices and making it accessible to a broad audience.

    In this post, we will create a basic iOS application that reads from and writes to an NFC tag.

    Requirements

    To successfully use this technology, two requirements must be met:
    1. iOS Device Compatibility: You need to deploy it on a real iOS device running iOS 13 or later. All iPhone 7 models and newer can read and write NFC tags.
    2. NFC Tags: Ensure that the NFC tags you use are compatible with iOS. I’ve purchased these tags—double-check their compatibility if you decide to experiment with them.

    Base project and NFC configuration

    Setting up NFC on any iOS app requires a minimum of two steps. The first step is to set the ‘NFC scan usage description’ text message in Build settings (or in the Info.plist file if you’re working with an older iOS project).

    The second enhancement is to add ‘Near Field Communication (NFC) Tag’ capability to the signing capabilities.

    Finally setup entitlements for allowing working with NDEF tags:

    NFC sample application

    The app features a straightforward interface consisting of an input box for entering the value to be stored on the NFC tag, a button for reading, and another for writing. At the bottom, it displays the value retrieved from the tag.

    From the coding perspective, the app serves as both a view and a manager for handling NFC operations. Below is an introduction to the NFC Manager:

    final class NFCManager: NSObject, ObservableObject,
                            @unchecked Sendable  {
        
        @MainActor
        static let shared = NFCManager()
        @MainActor
        @Published var tagMessage = ""
        
        private var internalTagMessage: String = "" {
            @Sendable didSet {
                Task { [internalTagMessage] in
                    await MainActor.run {
                        self.tagMessage = internalTagMessage
                    }
                }
            }
        }
        
        var nfcSession: NFCNDEFReaderSession?
        var isWrite = false
        private var userMessage: String?
        
        @MainActor override init() {
        }
    }

    The code is compatible with Swift 6. I had to rollback the use of @GlobalActor for this class because some delegated methods were directly causing the app to crash. The tagMessage attribute, which holds the content of the NFC tag, is a @Published property that is ultimately displayed in the view.

    This attribute is marked with @MainActor, but the Manager operates in a different, isolated domain. To avoid forcing updates to this attribute on @MainActor directly from any delegated method, I created a mirrored property, internalTagMessage. This property resides in the same isolated domain as the NFC Manager. Whenever internalTagMessage is updated, its value is then safely transferred to @MainActor. This approach ensures that the delegate methods remain cleaner and avoids cross-domain synchronization issues.

    // MARK :- NFCManagerProtocol
    extension NFCManager: NFCManagerProtocol {
        
        func startReading() async {
            self.nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
            self.isWrite = false
            self.nfcSession?.begin()
        }
        
        func startWriting(message: String) async {
            nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
            isWrite = true
            userMessage = message
            nfcSession?.begin()
        }
    }

    The NFCManagerProtocol defines the operations requested by the view. Each time a new read or write operation is initiated, a new NFC NDEF reader session is started, and the relevant delegate methods are invoked to handle the operation.

    // MARK :- NFCNDEFReaderSessionDelegate
    extension NFCManager:  NFCNDEFReaderSessionDelegate {
    
        func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
    
        }
        
        func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
            guard let tag = tags.first else { return }
            
            session.connect(to: tag) { error in
                if let error = error {
                    session.invalidate(errorMessage: "Connection error: \(error.localizedDescription)")
                    return
                }
                
                tag.queryNDEFStatus { status, capacity, error in
                    guard error == nil else {
                        session.invalidate(errorMessage: "Error checking NDEF status")
                        return
                    }
                    
                    switch status {
                    case .notSupported:
                        session.invalidate(errorMessage: "Not compatible tat")
                    case  .readOnly:
                        session.invalidate(errorMessage: "Tag is read-only")
                    case .readWrite:
                        if self.isWrite {
                            self.write(session: session, tag: tag)
                        } else {
                            self.read(session: session, tag: tag)
                        }
                        
                    @unknown default:
                        session.invalidate(errorMessage: "Unknown NDEF status")
                    }
                }
            }
        }
        
        private func read(session: NFCNDEFReaderSession, tag: NFCNDEFTag) {
            tag.readNDEF { [weak self] message, error in
                if let error {
                    session.invalidate(errorMessage: "Reading error: \(error.localizedDescription)")
                    return
                }
                
                guard let message else {
                    session.invalidate(errorMessage: "No recrods found")
                    return
                }
                
                if let record = message.records.first {
                    let tagMessage = String(data: record.payload, encoding: .utf8) ?? ""
                    print(">>> Read: \(tagMessage)")
                    session.alertMessage = "ReadingSucceeded: \(tagMessage)"
                    session.invalidate()
                    self?.internalTagMessage = tagMessage
                }
            }
        }
        
        private func write(session: NFCNDEFReaderSession, tag: NFCNDEFTag) {
            guard let userMessage  = self.userMessage else { return }
            let payload = NFCNDEFPayload(
                format: .nfcWellKnown,
                type: "T".data(using: .utf8)!,
                identifier: Data(),
                payload: userMessage.data(using: .utf8)!
            )
            let message = NFCNDEFMessage(records: [payload])
            tag.writeNDEF(message) { error in
                if let error = error {
                    session.invalidate(errorMessage: "Writing error: \(error.localizedDescription)")
                } else {
                    print(">>> Write: \(userMessage)")
                    session.alertMessage = "Writing succeeded"
                    session.invalidate()
                }
            }
        }
        
        func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {}
        
        func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
            print( "Session Error: \(error.localizedDescription)")
        }
    }
    • readerSession(_:didDetectNDEFs:) This method is a placeholder for handling detected NDEF messages. Currently, it doesn’t contain implementation logic.
    • readerSession(_:didDetect:) This method is triggered when NFC tags are detected. It connects to the first detected tag and determines its NDEF status (read/write capabilities). Depending on the status, it decides whether to read or write data using the read or write methods.
    • readerSessionDidBecomeActive(_:) This method is called when the NFC reader session becomes active. It has no custom logic here.
    • readerSession(_:didInvalidateWithError:) This method handles session invalidation due to errors, logging the error message.

    Finally, deploying the app on a real device should exhibit the following behavior:

    Store the text «Hello world!» in an NFC tag. Later, retrieve the text from the tag and display it at the bottom of the view.

    Conclusions

    This example takes a minimalist approach to demonstrate how easy it is to start experimenting with this technologyy.You can find source code used for writing this post in following repository.

    References

  • Dealing a REST API with Combine

    Dealing a REST API with Combine

    Combine is a framework introduced by Apple in iOS 13 (as well as other platforms like macOS, watchOS, and tvOS) that provides a declarative Swift API for processing values over time. It simplifies working with asynchronous programming, making it easier to handle events, notifications, and data streams.

    In this post, we will focus on Publishers when the source of data is a REST API. Specifically, we will implement two possible approaches using Publisher and Future, and discuss when it is better to use one over the other.

    Starting Point for Base Code

    The starting base code is the well-known Rick and Morty sample list-detail iOS app, featured in the DebugSwift post, Streamline Your Debugging Workflow.

    From that point, we will implement the Publisher version, followed by the Future version. Finally, we will discuss the scenarios in which each approach is most suitable.

    Publisher

    Your pipeline always starts with a publisher, the publisher handles the «producing» side of the reactive programming model in Combine.

    But lets start from the top view,  now  comment out previous viewmodel call for fetching data and call the new one based on Combine.

            .onAppear {
    //            Task {
    //                 await viewModel.fetch()
    //            }
                viewModel.fetchComb()
            }

    This is a fetch method for the view model, using the Combine framework:

    import SwiftUI
    @preconcurrency import Combine
    
    @MainActor
    final class CharacterViewModel: ObservableObject {
        @Published var characters: [Character] = []
        
        var cancellables = Set<AnyCancellable>()
    
           ...
    
        func fetchComb() {
            let api = CharacterServiceComb()
            api.fetch()
                .sink(receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        print("Fetch successful")
                    case .failure(let error):
                        print("Error fetching data: \(error)")
                    }
                }, receiveValue: { characters in
                    self.characters = characters.results.map { Character($0) }
                })
                .store(in: &cancellables)
        }
    }

    The fetchComb function uses the Combine framework to asynchronously retrieve character data from an API service. It initializes an instance of CharacterServiceComb and calls its fetch() method, which returns a publisher. The function uses the sink operator to handle responses: it processes successful results by printing a message and mapping the data into Character objects, while logging any errors that occur in case of failure.

    The subscription to the publisher is stored in the cancellables set, which manages memory and ensures the subscription remains active. When the subscription is no longer needed, it can be cancelled. This pattern facilitates asynchronous data fetching, error management, and updating the app’s state using Combine’s declarative style.

    Let’s dive into CharacterServiceComb:

    import Combine
    import Foundation
    
    final class CharacterServiceComb {
    
        let baseService = BaseServiceComb<ResponseJson<CharacterJson>>(param: "character")
        
        func fetch() -> AnyPublisher<ResponseJson<CharacterJson>, Error>  {
            baseService.fetch()
        }
    }

    Basically, this class is responsible for creating a reference to CharacterServiceComb, which is the component that actually performs the REST API fetch. It also sets up CharacterServiceComb for fetching character data from the service and retrieving a ResponseJson<CharacterJson> data structure.

    Finally, CharacterServiceComb:

        func fetch() -> AnyPublisher<T, Error> {
            guard let url = BaseServiceComb<T>.createURLFromParameters(parameters: [:], pathparam: getPathParam()) else {
                return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
            }
            
            return URLSession.shared.dataTaskPublisher(for: url)
                .map(\.data)
                .decode(type: T.self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }

    It begins by constructing a URL using parameters and a path parameter. If the URL is valid, it initiates a network request using URLSession.shared.dataTaskPublisher(for:), which asynchronously fetches data from the URL. The response data is then mapped to a type T using JSONDecoder, and the result is sent to the main thread using .receive(on: DispatchQueue.main). Finally, the publisher is erased to AnyPublisher<T, Error> to abstract away the underlying types.

    Finally, build and run the app to verify that it is still working as expected.

    Future

    The Future publisher will publish only one value and then the pipeline will close. When the value is published is up to you. It can publish immediately, be delayed, wait for a user response, etc. But one thing to know about Future is that it only runs one time.

    Again lets start from the top view, comment out previous fetchComb and call the new one fetchFut based on Future

            .onAppear {
    //            Task {
    //                 await viewModel.fetch()
    //            }
    //            viewModel.fetchComb()
                viewModel.fetchFut()
            }

    This is a fetch method for the view model that use the future:

    import SwiftUI
    @preconcurrency import Combine
    
    @MainActor
    final class CharacterViewModel: ObservableObject {
        @Published var characters: [Character] = []
        
        var cancellables = Set<AnyCancellable>()
            
        ...
        
        func fetchFut() {
            let api = CharacterServiceComb()
        api.fetchFut()
                .sink(receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        print("Fetch successful")
                    case .failure(let error):
                        print("Error fetching data: \(error)")
                    }
                }, receiveValue: { characters in
                    self.characters = characters.results.map { Character($0) }
                })
                .store(in: &cancellables)
        }
    }

    This code defines a function fetchFut() that interacts with an API to fetch data asynchronously. It first creates an instance of CharacterServiceComb, which contains a method fetchFut() that returns a Future. The sink operator is used to subscribe to the publisher and handle its result. The receiveCompletion closure handles the completion of the fetch operation: it prints a success message if the data is fetched without issues, or an error message if a failure occurs.

    The receiveValue closure processes the fetched data by mapping the results into Character objects and assigning them to the characters property. The subscription is stored in cancellables to manage memory and lifecycle, ensuring that the subscription remains active and can be cancelled if necessary.

    final class CharacterServiceComb {
    
        let baseService = BaseServiceComb<ResponseJson<CharacterJson>>(param: "character")
        
        func fetch() -> AnyPublisher<ResponseJson<CharacterJson>, Error>  {
            baseService.fetch()
        }
        
        func fetchFut() -> Future<ResponseJson<CharacterJson>, Error> {
            baseService.fetchFut()
        }
    }

    The fetchFut() function now returns a Future instead of a Publisher.

    Finally, CharacterServiceComb:

    func fetchFut() -> Future<T, Error> {
            return Future { ( promise: @escaping (Result<T, Error>) -> Void) in
                nonisolated(unsafe) let promise = promise
    
                    guard let url = BaseServiceComb<T>.createURLFromParameters(parameters: [:], pathparam: self.getPathParam())else {
                        return promise(.failure(URLError(.badURL)))
                    }
    
                    let task = URLSession.shared.dataTask(with: url) { data, response, error in
                        Task { @MainActor in
                            guard let httpResponse = response as? HTTPURLResponse,
                                  (200...299).contains(httpResponse.statusCode) else {
                                promise(.failure(ErrorService.invalidHTTPResponse))
                                return
                            }
                            
                            guard let data = data else {
                                promise(.failure(URLError(.badServerResponse)))
                                return
                            }
                            
                            do {
                                let dataParsed: T = try JSONDecoder().decode(T.self, from: data)
                                promise(.success(dataParsed))
                            } catch {
                                promise(.failure(ErrorService.failedOnParsingJSON))
                                return
                            }
                        }
                    }
                    task.resume()
            }
        }
     

    The provided code defines a function fetchFut() that returns a Future object, which is a type that represents a value that will be available in the future. It takes no input parameters and uses a closure (promise) to asynchronously return a result, either a success or a failure. When the URL is valid, then, it initiates a network request using URLSession.shared.dataTask to fetch data from the generated URL.

    Once the network request completes, when the response is valid  data is received, it attempts to decode the data into a specified type T using JSONDecoder. If decoding is successful, the promise is resolved with the decoded data (.success(dataParsed)), otherwise, it returns a parsing error. The code is designed to work asynchronously and to update the UI or handle the result on the main thread (@MainActor). This is perfomed in that way becasue future completion block is exectued in main thread, so for still woring with promise we have to force to continue task in @MainActor.

    Publisher vs Future

    In iOS Combine, a Future is used to represent a single asynchronous operation that will eventually yield either a success or a failure. It is particularly well-suited for one-time results, such as fetching data from a network or completing a task that returns a value or an error upon completion. A Future emits only one value (or an error) and then completes, making it ideal for scenarios where you expect a single outcome from an operation.

    Conversely, a Publisher is designed to handle continuous or multiple asynchronous events and data streams over time. Publishers can emit a sequence of values that may be finite or infinite, making them perfect for use cases like tracking user input, listening for UI updates, or receiving periodic data such as location updates or time events. Unlike Futures, Publishers can emit multiple values over time and may not complete unless explicitly cancelled or finished, allowing for more dynamic and ongoing data handling in applications.

    Conclusions

    In this exaple is clear that better approach is Future implementation.You can find source code used for writing this post in following repository.

    References

  • Safely Gathering Singletons While Avoiding Data Races

    Safely Gathering Singletons While Avoiding Data Races

    The text is clear and conveys the intended message effectively. However, it can be slightly refined for improved readability and flow. Here’s a polished version: In our previous post, we discussed migrating an app that uses a Singleton to Swift 6.0. In this post, we’ll focus on consolidating multiple multipurpose Singletons into a single access point. This approach simplifies unit testing by enabling the injection of mocked Singletons.

    Base project

    We begin where we left off in the iOS Location Manager: A Thread-Safe Approach post. In that post, we explained how to migrate a Location Manager. Now, we’ll create a new blank project, ensuring that the Swift testing target is included.

    The base code is the source code provided in the commented section of the post. At the end of the post, you will find a link to the GitHub repository. By reviewing its history, you can trace back to this point.

    At this stage, we will create a second singleton whose purpose is to manage a long-running background task.

    @globalActor
    actor GlobalManager {
        static var shared = GlobalManager()
    }
    
    protocol LongTaskManagerProtocol {
        @MainActor var isTaskDone: Bool { get }
        func doLongTask() async
    }
    
    @GlobalManager
    class LongTaskManager: ObservableObject, LongTaskManagerProtocol {
    
        @MainActor
        static let shared = LongTaskManager()
    
        @MainActor
        @Published var isTaskDone: Bool = false
        
        private var isTaskDoneInternal: Bool = false {
            didSet {
                Task {
                    await MainActor.run { [isTaskDoneInternal] in
                        isTaskDone = isTaskDoneInternal
                    }
                }
            }
        }
    
        #if DEBUG
        @MainActor
        /*private*/ init() {
        }
        #else
        @MainActor
        private init() {
        }
        #endif
        
        // MARK :- LongTaskManagerProtocol
        func doLongTask() async {
            isTaskDoneInternal = false
            print("Function started...")
            // Task.sleep takes nanoseconds, so 10 seconds = 10_000_000_000 nanoseconds
            try? await Task.sleep(nanoseconds: 10_000_000_000)
            print("Function finished!")
            isTaskDoneInternal = true
        }
    }

    Key Concepts at Work

    1. Actor Isolation:

      • Ensures thread safety and serializes access to shared state (isTaskDoneInternal) through GlobalManager.
      • @MainActor guarantees main-thread access for UI-related properties and tasks.
    2. SwiftUI Integration:

      • @Published with ObservableObject enables reactive UI updates.
    3. Encapsulation:

      • Internal state (isTaskDoneInternal) is decoupled from the externally visible property (isTaskDone).
    4. Concurrency-Safe Singleton:

      • The combination of @MainActor, @GlobalManager, and private init creates a thread-safe singleton usable across the application.
     
    We will now make minimal changes to ContentView to integrate and provide visibility for this new Singleton.
    struct ContentView: View {
        @StateObject private var locationManager = LocationManager.shared
        @StateObject private var longTaskManager = LongTaskManager.shared
        
        var body: some View {
            VStack(spacing: 20) {
                Text("LongTask is \(longTaskManager.isTaskDone ? "done" : "running...")")
               ...
            .onAppear {
                locationManager.checkAuthorization()
                
                
                Task {
                  await longTaskManager.doLongTask()
                }
            }
            .padding()
        }
    }

    Key Concepts at Work

    1. Singleton Reference:
      Use a singleton reference to the LongTaskManager.
      The @StateObject property wrapper ensures that any changes in LongTaskManager.isTaskDone automatically update the ContentView.

    2. LongTaskManager Execution Status:
      The longTaskManager.isTaskDone property determines the message displayed based on the execution status.

    3. Start Long Task:
      The .onAppear modifier is the appropriate place to invoke longTaskManager.doLongTask().

    4. Testing on a Real Device:
      Build and deploy the app on a real device (iPhone or iPad) to observe the long task execution. You’ll notice that it takes a while for the task to complete.

    All the Singletons came together at one location

    During app development, there may come a point where the number of Singletons in your project starts to grow uncontrollably, potentially leading to maintenance challenges and reduced code manageability. While Singletons offer advantages—such as providing centralized access to key functionality (e.g., Database, CoreLocation, AVFoundation)—they also have notable drawbacks:

    1. Global State Dependency: Code relying on a Singleton is often dependent on global state, which can lead to unexpected behaviors when the state is altered elsewhere in the application.
    2. Challenges in Unit Testing: Singletons retain their state across tests, making unit testing difficult and prone to side effects.
    3. Mocking Limitations: Replacing or resetting a Singleton for testing purposes can be cumbersome, requiring additional mechanisms to inject mock instances or reset state.

    To address these challenges, the following Swift code defines a struct named AppSingletons. This struct serves as a container for managing singletons, simplifying dependency injection and promoting better application architecture.

    import Foundation
    
    struct AppSingletons {
        var locationManager: LocationManager
        var longTaskManager: LongTaskManager
        
        init(locationManager: LocationManager = LocationManager.shared,
             longTaskManager: LongTaskManager = LongTaskManager.shared) {
            self.locationManager = locationManager
            self.longTaskManager = longTaskManager
        }
    }
     var appSingletons = AppSingletons()

    Ensure that singleton references are obtained from appSinglegons.

    struct ContentView: View {
        @StateObject private var locationManager = appSingletons.locationManager
        @StateObject private var longTaskManager = appSingletons.longTaskManager
        
        var body: some View {

    After performing a sanity check to ensure everything is working, let’s move on to the test target and add the following unit test:

    import Testing
    @testable import GatherMultipleSingletons
    
    struct GatherMultipleSingletonsTests {
    
        @Test @MainActor func example() async throws {
            let longTaskManagerMock = LongTaskManagerMock()
            appSingletons = AppSingletons(longTaskManager: longTaskManagerMock)
            #expect(appSingletons.longTaskManager.isTaskDone == false)
            await appSingletons.longTaskManager.doLongTask()
            #expect(appSingletons.longTaskManager.isTaskDone == true)
        }
    
    }
    
    final class LongTaskManagerMock: LongTaskManager {
        
        override func doLongTask() async {
            await MainActor.run {
                isTaskDone = true
            }
        }
    }
    The test verifies the behavior of a mock implementation of a singleton when performing a long task. It is likely part of verifying the integration between AppSingleton and LongTaskManager, ensuring that the singleton’s behavior matches expectations under controlled test conditions. By using the mock, the test becomes predictable and faster, avoiding the need for actual long-running logic.

    …Thread safe touch

    Now is time to turn  this code into a thread safe. Set Swift Concurrency Checking to Complete:

    … and Swift language version to Swift 6.

    The first issue we identified is that, from a non-isolated domain, the struct is attempting to access an isolated one (@MainActor). Additionally, appSingletons is not concurrency-safe because, as mentioned, it resides in a non-isolated domain.

    ContentView (@MainActor) is currently accessing this structure directly. The best approach would be to move the structure to an @MainActor-isolated domain.

    import Foundation
    
    @MainActor
    struct AppSingletons {
        var locationManager: LocationManager
        var longTaskManager: LongTaskManager
        
        init(locationManager: LocationManager = LocationManager.shared,
             longTaskManager: LongTaskManager = LongTaskManager.shared) {
            self.locationManager = locationManager
            self.longTaskManager = longTaskManager
        }
    }
    
    @MainActor var appSingletons = AppSingletons()

    This means that the LongTaskManager is executed only within the @MainActor. However, this isn’t entirely true. The part responsible for accessing shared attributes and updating the @Published property is executed under the @MainActor, but the part performing the heavy lifting runs in a @globalActor isolated domain.

    Conclusions

    In this post I have showed a way avoid Singleton discontrol, by gathering them in a global structure. You can find the source code used in this post in the following repository.

    References

  • iOS Location Managers: A Thread-Safe Approach

    iOS Location Managers: A Thread-Safe Approach

    The aim of this post is just to explain how to migrate any app that uses CoreLocation to Swift 6.0. First step will be create a simple app that presents current location and later on we will close the post with the migration.

    CoreLocation

    Core Location Framework Overview

    Core Location is an iOS framework that enables apps to access and utilize a device’s geographic location, altitude, and orientation. It provides robust services for location-based functionalities, leveraging device components such as Wi-Fi, GPS, Bluetooth, cellular hardware, and other sensors.

    Key Functionalities of Core Location:

    1. Location Services:
      • Standard Location Service: Tracks user location changes with configurable accuracy.
      • Significant Location Service: Provides updates for significant location changes.
    2. Regional Monitoring: Monitors entry and exit events for specific geographic regions.
    3. Beacon Ranging: Detects and tracks nearby iBeacon devices.
    4. Visit Monitoring: Identifies locations where users spend significant periods of time.
    5. Compass Headings: Tracks the user’s directional heading.
    6. Altitude Information: Supplies data about the device’s altitude.
    7. Geofencing: Enables the creation of virtual boundaries that trigger notifications upon entry or exit.

    iOS Location sample app:

    Create a new blank iOS SwiftUI APP.

    This Swift code defines a class named LocationManager that integrates with Apple’s Core Location framework to handle location-related tasks such as obtaining the user’s current coordinates and resolving the corresponding address. Below is a breakdown of what each part of the code does

    import Foundation
    import CoreLocation
    
    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
        
        static let shared = LocationManager()
        
        private var locationManager = CLLocationManager()
        private let geocoder = CLGeocoder()
        
        @Published var currentLocation: CLLocationCoordinate2D?
        @Published var currentAddress: CLPlacemark?
        
        private override init() {
            super.init()
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
        }
        
        func checkAuthorization() {
            switch locationManager.authorizationStatus {
            case .notDetermined:
                locationManager.requestWhenInUseAuthorization()
            case .restricted, .denied:
                print("Location access denied")
            case .authorizedWhenInUse, .authorizedAlways:
                locationManager.requestLocation()
            @unknown default:
                break
            }
        }
        
        func requestLocation() {
            locationManager.requestLocation()
        }
        
        func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
            print("Failed to find user's location: \(error.localizedDescription)")
        }
        
        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
            checkAuthorization()
        }
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            if let location = locations.first {
                self.currentLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude,
                                                              longitude: location.coordinate.longitude)
                reverseGeocode(location: location)
            }
        }
        
        private func reverseGeocode(location: CLLocation) {
            geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in
                if let placemark = placemarks?.first, error == nil {
                    self?.currentAddress = CLPlacemark(placemark: placemark)
                } else {
                    print("Error during reverse geocoding: \(error?.localizedDescription ?? "Unknown error")")
                }
            }
        }
    }
    

    Main Responsabilites and features:

    1. Singleton Pattern

      • The class uses a shared instance (LocationManager.shared) to provide a global access point.
      • The initializer (private override init()) is private to enforce a single instance.
    1. CoreLocation Setup

      • CLLocationManager: Manages location-related activities (e.g., obtaining current location, monitoring location updates).
      • CLGeocoder: Converts geographic coordinates to human-readable addresses (reverse geocoding).
    2. Published Properties

      • @Published: Allows properties (currentLocation and currentAddress) to trigger UI updates in SwiftUI whenever they change.
    3. Authorization Handling

      • Checks and requests location permissions (checkAuthorization()).
      • Responds to changes in authorization status (locationManagerDidChangeAuthorization).
    4. Requesting Location

      • requestLocation(): Asks CLLocationManager to fetch the current location.
    5. Delegate Methods

      • Handles success (didUpdateLocations) and failure (didFailWithError) when fetching the location.
      • Updates currentLocation with the retrieved coordinates.
      • Performs reverse geocoding to convert coordinates to a readable address (reverseGeocode(location:)).
    6. Reverse Geocoding

      • Converts a CLLocation into a CLPlacemark (e.g., city, street, country).
      • Updates currentAddress on success or logs an error if reverse geocoding fails.

    How it Works in Steps

    1. Initialization

      • The singleton instance is created (LocationManager.shared).
      • CLLocationManager is set up with a delegate (self) and a desired accuracy.
    2. Authorization

      • The app checks location permissions using checkAuthorization().
      • If permission is undetermined, it requests authorization (requestWhenInUseAuthorization()).
      • If authorized, it requests the user’s current location (requestLocation()).
    3. Location Fetch

      • When a location update is received, didUpdateLocations processes the first location in the array.
      • The geographic coordinates are stored in currentLocation.
      • The reverseGeocode(location:) method converts the location to an address (currentAddress).
    4. Error Handling

      • Location fetch errors are logged via didFailWithError.
      • Reverse geocoding errors are logged in reverseGeocode.

    Finally we’re are going to request some location data from content view:

    struct ContentView: View {
        @StateObject private var locationManager = LocationManager()
        
        var body: some View {
            VStack(spacing: 20) {
                if let location = locationManager.currentLocation {
                    Text("Latitude: \(location.latitude)")
                    Text("Longitude: \(location.longitude)")
                } else {
                    Text("Location not available")
                }
                
                if let address = locationManager.currentAddress {
                    Text("Name: \(address.name ?? "Unknown")")
                    Text("Town: \(address.locality ?? "Unknown")")
                    Text("Country: \(address.country ?? "Unknown")")
                } else {
                    Text("Address not available")
                }
                
                Button(action: {
                    locationManager.requestLocation()
                }) {
                    Text("Request Location")
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
            }
            .onAppear {
                locationManager.checkAuthorization()
            }
            .padding()
        }
    }

    Last but not least be sure that ContentView is executing the view that we have just created. And be sure that you have a description for NSLocationWhenInUseUsageDescription setting.

    To run the app, ensure it is deployed on a real device (iPhone or iPad). When the app prompts for permission to use location services, make sure to select «Allow.»

    …Thread safe approach

    This is the Side B of the post—or in other words, the part where we save the thread! 😄 Now, head over to the project settings and set Strict Concurrency Checking to Complete.

    … and Swift language version to Swift 6.

    The first issue we identified is that the LocationManager is a singleton. This design allows it to be accessed from both isolated domains and non-isolated domains.

    In this case, most of the helper methods are being called directly from views, so it makes sense to move this class to @MainActor.

    @MainActor
    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
     

    Now is the time to examine the data returned in the delegate methods. Our delegate methods do not modify the data, but some of them forward a copy of the received data. With our current implementation, this ensures that we avoid data races.

    In computer science, there are no «silver bullets,» and resolving issues when migrating from Swift 6 is no exception. When reviewing library documentation, if it is available, you should identify the specific domain or context from which the library provides its data. For Core Location, for instance, ensure that the CLLocationManager operates on the same thread on which it was initialized.

    We have a minimum set of guarantees to establish the protocol as @preconcurrency.

    @MainActor
    class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate {
     

    At this point, we fulfill the Swift 6 strict concurrency check requirements. By marking the singleton variable as @MainActor, we fix both of the previous issues at once.

    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
       
        @MainActor
        static let shared = LocationManager()
     

    Fixing migration issues is an iterative task. The more you work on it, the faster you can find a solution, but sometimes there is no direct fix. Build and deploy on a real device to ensure everything is working as expected.

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

    Conclusions

    In this post, you have seen how easy is to migrate CoreLocationManager

    References