Autor: admin

  • Dynamic Forms in SwiftUI for variable section type

    Dynamic Forms in SwiftUI for variable section type

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

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

    Dynamic sample SwiftUI app

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

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

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

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

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

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

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

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

    Conclusions

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

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

    References

  • 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

  • Bridging  Data Transfer from WKWebView to iOS

    Bridging Data Transfer from WKWebView to iOS

    The aim of this post is to bridge the gap between web technologies and native iOS development by enabling data transfer from the web side to the app. In some native apps, it is common to have a WebView control rendering web content, and it is not unusual for the app to require data from the web content for further tasks.

    In this post, we simulate a local web server using a Docker container running an HTML+JavaScript page that displays a button. When the button is pressed, a message is sent and captured by the app.

    Web content and web server

    Web content is basically this HTML+JavaScript code:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Communication with SwiftUI</title>
    </head>
    <body>
        <h1>Hello world!</h1>
        <p>This is a HTML page served from a Docker container with Nginx.</p>
        <button id="sendDataBtn">Send Data to App</button>
    
        <script>
            document.getElementById("sendDataBtn").addEventListener("click", function() {
                var data = "Hello from JavaScript!";
                // Send data to the native app
                window.webkit.messageHandlers.callbackHandler.postMessage(data);
            });
        </script>
    </body>
    </html>

     When the button is pressed, a message such as «Hello from JavaScript!» or any custom text of your choice is sent to the app.

    To serve this page, I have chosen Docker. Docker is an open-source platform that allows developers to automate the deployment, scaling, and management of applications within lightweight, portable containers. Containers encapsulate an application and its dependencies, ensuring consistent behavior across different environments, from development to production.

    By providing an isolated and reproducible environment, Docker resolves the classic «it works on my machine» issue. It enhances development efficiency, simplifies deployment processes, and streamlines testing, making it easier to scale and maintain applications across various systems or cloud infrastructures.

    Docker is a fascinating topic! If you’re unfamiliar with it, I highly recommend exploring some tutorials. At the end of this post, I’ve included a list of helpful references.

    Below is the Dockerfile we created to build the Docker image:

    # Nginx base image
    FROM nginx:alpine
    
    # Copy HTML file into container
    COPY index.html /usr/share/nginx/html/index.html
    
    # Expose port 80 (defect port for Nginx)
    EXPOSE 80
    

    This Dockerfile creates a Docker image that serves an HTML file using Nginx on a lightweight Alpine Linux base. Here’s a breakdown of each line:

    1. FROM nginx:alpine:
      This line specifies the base image to use for the Docker container. It uses the official nginx image with the alpine variant, which is a minimal version of Nginx built on the Alpine Linux distribution. This results in a small and efficient image for running Nginx.

    2. COPY index.html /usr/share/nginx/html/index.html:
      This line copies the index.html file from your local directory (where the Dockerfile is located) into the container’s filesystem. Specifically, it places the index.html file into the directory where Nginx serves its static files (/usr/share/nginx/html/). This file will be accessible when the container runs, and Nginx will serve it as the default webpage.

    3. EXPOSE 80:
      This instruction tells Docker that the container will listen on port 80, which is the default port for HTTP traffic. It doesn’t actually publish the port but serves as documentation for which port the container expects to use when run. This is helpful for networking and linking with other containers or exposing the container’s services to the host machine.

    To create the Docker image, open a terminal window in the directory containing the Dockerfile and run:

    $ docker build -t web-server .

    The command docker build -t web-server . builds a Docker image from the Dockerfile in the current directory (.). The resulting image is tagged with the name web-server.

    The web content has been embedded within the image. Therefore, if you modify the content, you will need to recreate the image.

    The next step is to run the container. In the context of programming, a container can be likened to creating an instance of an object.
    $ docker run -d -p 8080:80 web-server

    The command runs a Docker container in detached mode (-d) using the image web-server. It maps port 8080 on the host machine to port 80 inside the container (-p 8080:80)

    The container is now running. Open your favorite web browser and navigate to the following URL: ‘http://localhost:8080‘. The web content should load and be displayed.

    The iOS app

    iOS App basically presents a WebView controller:

    struct ContentView: View {
        @State private var messageFromJS: String = ""
        @State private var showAlert = false
        
        var body: some View {
            VStack {
                
                WebView(url: URL(string: "http://localhost:8080/")!) { message in
                    messageFromJS = message
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
            .onChange(of: messageFromJS) {
                showAlert.toggle()
            }
            .alert(isPresented: $showAlert) {
                Alert(
                    title: Text("Message from JavaScript:"),
                    message: Text("\(messageFromJS)"),
                    dismissButton: .default(Text("OK"))
                )
            }
        }
    }

    If we take a look at WebView:

    import SwiftUI
    import WebKit
    
    struct WebView: UIViewRepresentable {
        var url: URL
        var onMessageReceived: (String) -> Void // Closure to handle messages from JS
    
        class Coordinator: NSObject, WKScriptMessageHandler {
            var parent: WebView
    
            init(parent: WebView) {
                self.parent = parent
            }
    
            // This method is called when JS sends a message to native code
            func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
                if message.name == "callbackHandler" {
                    if let messageBody = message.body as? String {
                        parent.onMessageReceived(messageBody)
                    }
                }
            }
        }
    
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
    
        func makeUIView(context: Context) -> WKWebView {
            let configuration = WKWebViewConfiguration()
            configuration.userContentController.add(context.coordinator, name: "callbackHandler")
    
            let webView = WKWebView(frame: .zero, configuration: configuration)
            webView.load(URLRequest(url: url))
            return webView
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            // No need to update the WebView in this case
        }
    }

    The code defines a WebView struct that integrates a WKWebView (a web view) into a SwiftUI interface. The WebView struct conforms to the UIViewRepresentable protocol, allowing it to present UIKit components within SwiftUI. A custom coordinator class (Coordinator) is set up to handle messages sent from JavaScript running inside the web view. Specifically, when JavaScript sends a message using the name "callbackHandler", the userContentController(_:didReceive:) method is triggered. This method passes the message to a closure (onMessageReceived) provided by the WebView, enabling custom handling of the message.

    The makeUIView method creates and configures the WKWebView, including loading a specified URL to display the desired web content. When the project is deployed in a simulator, the web content is rendered properly, demonstrating the effectiveness of this integration.

    This implementation provides a powerful way to integrate web content into a SwiftUI application, enabling dynamic interaction between SwiftUI and JavaScript.

    When we press the ‘Send Data to App’ button:

    Is presented an alert with the message sent from web content.

    Conclusions

    In this post, I have preseted a way to pass information from web to iOS App. In this app we have transfered non-sensitive information because passing information from web content in a WebView to an iOS app poses several security risks, including Cross-Site Scripting (XSS) attacks, data leakage, injection attacks, unauthorized file system access, URL scheme abuse, and mixed content issues. These vulnerabilities can lead to unauthorized access to user data, compromise of app integrity, and exposure of sensitive information. To mitigate these risks, developers should use WKWebView, implement input sanitization, enforce content security policies, use HTTPS, disable unnecessary JavaScript execution, and properly configure WebView restrictions. By adhering to these security practices, developers can significantly reduce the attack surface and enhance the overall security of their iOS applications.

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

    References

  • iOS NFC Development: From URLs to Deeplinks

    iOS NFC Development: From URLs to Deeplinks

    Writing a URL or deep link into an NFC tag enables seamless integration between the physical and digital worlds. It offers instant access to online content and enhanced user experiences. Additionally, it creates automation opportunities, simplifying interactions such as opening web pages, accessing app-specific features, or triggering IoT actions. These capabilities make NFC tags valuable for marketing, smart environments, and personalization. This technology finds applications in retail, events, tourism, and healthcare, bringing convenience, innovation, and a modern touch.

    In this post, we will continue evolving the app created in the “Harnessing NFC Technology in Your iOS App” post by adding two more functionalities: one for storing a regular web URL and another for adding a deep link to open the same app. By the end of this guide, you’ll be equipped to expand your app’s NFC capabilities and create an even more seamless user experience.

    Storing web url into NFC tag

    Add a new function to handle the write URL operation:
        func startWritingURL() async {
            nfcOperation = .writeURL
            startSesstion()
        }
        
        private func startSesstion() {
            nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
            nfcSession?.begin()
        }
    We ran out of Boolean operations, so I created an enum to implement the three current NFC operations: read, write, and write URL. For this process, we set the operation to perform and initiate an NFC session.
    The readerSession delegate function handles connecting to the NFC tag and querying its status.
    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:
                        switch self.nfcOperation {
                        case .read:
                            self.read(session: session, tag: tag)
                        case .write:
                            self.write(session: session, tag: tag)
                        case .writeURL:
                            self.writeUrl(session: session, tag: tag)
                        }
                        
                    @unknown default:
                        session.invalidate(errorMessage: "Unknown NDEF status")
                    }
                }
            }
        }
    When a writable NFC tag is detected and the operation is set to .writeURL, the method responsible for writing the URL to the tag will be called.
        private func writeUrl(session: NFCNDEFReaderSession, tag: NFCNDEFTag) {
            guard let url = URL(string: "https://javios.eu/portfolio/"),
                let payload = NFCNDEFPayload.wellKnownTypeURIPayload(string: url.absoluteString) else {
                session.invalidate(errorMessage: "No se pudo crear el payload NDEF.")
                return
            }
    
            write(session, tag, payload) { error in
                guard  error == nil else { return }
                print(">>> Write: \(url.absoluteString)")
            }
        }
        
        private func write(_ session: NFCNDEFReaderSession,
                           _ tag: NFCNDEFTag,
                           _ nfcNdefPayload: NFCNDEFPayload, completion: @escaping ((Error?) -> Void)) {
            
            let NDEFMessage = NFCNDEFMessage(records: [nfcNdefPayload])
            tag.writeNDEF(NDEFMessage) { error in
                if let error = error {
                    session.invalidate(errorMessage: "Writing error: \(error.localizedDescription)")
                    completion(error)
                } else {
                    session.alertMessage = "Writing succeeded"
                    session.invalidate()
                    completion(nil)
                }
            }
        }
    
    This Swift code facilitates writing a URL as an NFC NDEF payload onto an NFC tag. The writeUrl function generates an NDEF payload containing a well-known type URI record that points to the URL «https://javios.eu/portfolio/«. If the payload is valid, the function invokes the write method, passing the NFC session, tag, and payload as parameters. The write function then creates an NFC NDEF message containing the payload and writes it to the NFC tag.
    Once the URL is placed within the tag, you can use the tag to open the web link, functioning similarly to scanning a QR code that redirects you to a website.

    Deeplinks

    A deeplink in iOS is a type of link that directs users to a specific location within an app, rather than just opening the app’s home screen. This helps enhance the user experience by providing a direct path to particular content or features within the app.

    In this example, we will create a deeplink that will open our current NFCApp directly:

    When iOS detects the ‘nfcreader://jca.nfcreader.open’ deep link, it will open the currently post development app on iOS.

    @main
    struct NFCAppApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .onOpenURL { url in
                    handleDeeplink(url: url)
                }
            }
        }
    
        func handleDeeplink(url: URL) {
            // Maneja el deeplink aquí
            print("Se abrió la app con el URL: \(url)")
        }
    }

    By adding the .onOpenURL modifier, the app will be able to detect when it is launched (or awakened) via a deep link.

    Finally, implement the deep link writing functionality by adapting the previously created writeUrl method:

        private func writeUrl(session: NFCNDEFReaderSession, tag: NFCNDEFTag, urlString: String) {
            guard let url = URL(string: urlString),
                let payload = NFCNDEFPayload.wellKnownTypeURIPayload(string: url.absoluteString) else {
                session.invalidate(errorMessage: "No se pudo crear el payload NDEF.")
                return
            }
    
            write(session, tag, payload) { error in
                guard  error == nil else { return }
                print(">>> Write: \(url.absoluteString)")
            }
        }

    It would be called in the following way for creating deeplink

                    case .readWrite:
                        switch self.nfcOperation {
                        case .read:
                            self.read(session: session, tag: tag)
                        case .write:
                            self.write(session: session, tag: tag)
                        case .writeURL:
                            self.writeUrl(session: session, tag: tag, urlString: "https://javios.eu/portfolio/")
                        case .writeDeeplink:
                            self.writeUrl(session: session, tag: tag, urlString: "nfcreader://jca.nfcreader.open")
                        }
                        

    Deploy project on a real device for validating behaviour

    Once the tag is written, when the deep link is triggered, the app will be closed and then reopened. You will be prompted to open the app again.

    Conclusions

    In this post, I have extended the functionalities we can implement with NFC tags by using URLs. 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

  • Dip your toes in middle of TCA ocean

    Dip your toes in middle of TCA ocean

    TCA, or The Composable Architecture, is a framework for iOS development that provides a structured and scalable approach to building robust, maintainable applications. Created by Brandon Williams and Stephen Celis, TCA leverages functional programming principles and Swift’s powerful type system to offer a modern solution for iOS app architecture.

    In this post, we’ll explore how to migrate our Rick and Morty iOS app to TC

    The architecture

    TCA consists of five main components:

    1. State: A single type that represents the entire state of an app or feature.
    2. Actions: An enumeration of all possible events that can occur in the app.
    3. Environment: A type that wraps all dependencies of the app or feature.
    4. Reducer: A function that transforms the current state to the next state based on a given action.
    5. Store: The runtime that powers the feature and manages the state.

    TCA offers several advantages for iOS development:

    • Unidirectional data flow: This makes it easy to understand how changes in state occur, simplifying debugging and preventing unexpected side effects.
    • Improved testability: TCA encourages writing features that are testable by default.
    • Modularity: It allows for composing separate features, enabling developers to plan, build, and test each part of the app independently.
    • Scalability: TCA is particularly useful for complex applications with many states and interactions.

    Configure XCode project

    To get started with this architecture, integrate the ComposableArchitecture library from its GitHub repository.

    Downloading might take some time.

    Character feature

    The component that we have to change implentation is basically the ViewModel component. In this case will be renamed as CharacterFeature.

    import ComposableArchitecture
    
    @Reducer
    struct CharatersFeature {
        @ObservableState
        struct State: Equatable {
            var characters: [Character] = []
            var isLoading: Bool = false
        }
    
        enum Action {
            case fetchCharacters
            case fetchCharactersSuccess([Character])
        }
    
        var body: some ReducerOf<Self> {
            Reduce { state, action in
                switch action {
                case .fetchCharacters:
                    state.isLoading = true
                    state.characters = []
                    return .run { send in
                        let result = await currentApp.dataManager.fetchCharacters(CharacterService())
                        switch result {
                        case .success(let characters):
                            //state.characters = characters
                            await send(.fetchCharactersSuccess(characters))
                        case .failure(let error):
                            print(error)
                        }
                    }
                case .fetchCharactersSuccess(let characters):
                    state.isLoading = false
                    state.characters = characters
                    return .none
                }
            }
        }
    }

    This code defines a feature using the Composable Architecture (TCA) framework in Swift. Let’s break down what this code does:

    1. Import and Structure:
      • It imports the ComposableArchitecture framework.
      • It defines a CharatersFeature struct with the @Reducer attribute, indicating it’s a reducer in the TCA pattern.
    2. State:
      • The State struct is marked with @ObservableState, making it observable for SwiftUI views.
      • It contains two properties:
        • characters: An array of Character objects.
        • isLoading: A boolean to track if data is being loaded.
    3. Actions:
      • The Action enum defines two possible actions:
        • fetchCharacters: Triggers the character fetching process.
        • fetchCharactersSuccess: Handles successful character fetching.
    4. Reducer:
      • The body property defines the reducer logic.
      • It uses a Reduce closure to handle state changes based on actions.
    5. Action Handling:
      • For .fetchCharacters:
        • Sets isLoading to true and clears the characters array.
        • Runs an asynchronous operation to fetch characters.
        • On success, it dispatches a .fetchCharactersSuccess action.
        • On failure, it prints the error.
      • For .fetchCharactersSuccess:
        • Sets isLoading to false.
        • Updates the characters array with the fetched data.
    6. Asynchronous Operations:
      • It uses .run for handling asynchronous operations within the reducer.
      • The character fetching is done using currentApp.dataManager.fetchCharacters(CharacterService()).

    This code essentially sets up a state management system for fetching and storing character data, with loading state handling. It’s designed to work with SwiftUI and the Composable Architecture, providing a structured way to manage application state and side effects.

    View

    The view is almost the same as before:

    struct CharacterView: View {
        let store: StoreOf<CharatersFeature>
        
        var body: some View {
            NavigationView {
                ZStack {
                    if store.isLoading {
                        ProgressView()
                    }
                ScrollView {
                        ForEach(store.characters) { character in
                            NavigationLink {
                                DetailView(character: character)
                            } label: {
                                HStack {
                                    characterImageView(character.imageUrl)
                                    Text("\(character.name)")
                                    Spacer()
                                }
                            }
                        }
                    }
                }
            }
            .padding()
            .onAppear {
                store.send(.fetchCharacters)
            }
        }

    The store holds observable items used by the view to present either the progression view or the character list. When the view appears, it triggers the .fetchCharacters action, prompting the reducer to fetch the character list.

    Unit test

    Unit testing with TCA differs significantly from my expectations:

        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let store = await TestStore(initialState: CharatersFeature.State()) {
                CharatersFeature()
            }
            
            await store.send(.fetchCharacters) {
              $0.isLoading = true
            }
            
            await store.receive(\.fetchCharactersSuccess, timeout: .seconds(1)) {
              $0.isLoading = false
                $0.characters = expCharacters
            }
            
            await store.finish()
        }

    In TCA, testing often focuses on asserting the state transitions and effects of the reducer. Instead of traditional XCTest assertions like XCTAssertEqual, TCA provides its own mechanism for testing reducers using TestStore, which is a utility designed to test state changes, actions, and effects in a deterministic way.

    Conclusions

    This is a very minimalistic example just to get in touch with this architecture. With more complex applications, I meain with some flows and many screens reducer would become a huge chunk of code, so god approach would be implement this pattern per app flow.You can find source code used for writing this post in following repository.

  • 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 migrating persisted models in iOS to prevent crashes

    Safely migrating persisted models in iOS to prevent crashes

    Most mobile native apps (iOS/Android) support evolving businesses, and must be ready to accommodate changes. Evolving persisted data in an app is crucial for maintaining a good user experience and preventing obsolescence as business requirements change. In my personal experience with production crashes, this is one of the most common issues encountered after a production release. While removing and reinstalling the app often fixes the issue, this is not an ideal solution.

    The aim of this post is to demonstrate a possible method for handling migration in non-database persisted data. We will explain how to migrate JSON stored in user defaults, though this approach could also apply to data stored in files or keychain. It’s important to note that we’re focusing on non-database storage because database frameworks typically provide their own migration mechanisms that should be followed.

    The starting point

    The base application used as the foundation for this post simply displays a button. When pressed, it saves a structure to UserDefaults. Upon restarting the app after it has been closed, the previously stored content is retrieved from UserDefaults.

    Let me introduce the persisted structure. It has been simplified for better understanding, but is not what you would find in a real production app:

    struct Person: Codable {
        let name: String
        let age: String
    }

    New requirements

    The business has decided that an email address is also required. Therefore, we will proceed to implement this feature.

    struct Person: Codable {
        let name: String
        let age: String
        let email: String
    }
    Build and run, but suddenly something unexpected happens…

    Initially, the app stored the data in the format {name, age}. However, it has since been updated to expect the format {name, age, email}. This discrepancy means that previously persisted data cannot be decoded properly, leading to an exception being thrown.

    A common mistake during development at this stage is to simply remove the app from the simulator, reinstall the new version, and move on, effectively ignoring the issue. This is a poor decision, as it fails to address the root problem. Eventually, this oversight will come back to haunt you when the same issue occurs on hundreds, thousands, or an unacceptably large number of real app installations. This will result in a very poor user experience.

    The first step to properly address this issue is to add a version field to the data structure. This allows for better handling of future changes to the structure.

    struct Person: Codable {
        var version: Int {
            return 1
        }
        let name: String
        let age: String
        let email: String
    }

    We will implement a Migration Manager responsible for handling migrations

    @MainActor
    protocol MigrationManagerProtocol {
        func applyMigration()
    }
    
    @MainActor
    final class MigrationManager: ObservableObject {
        @Published var migrationPenging = true
        @Published var migrationFailed = false
        
        struct PersonV0: Codable {
            let name: String
            let age: String
        }
        
        typealias PersonV1 = Person
    }

    In this class, we will include a copy of the original Person structure, referred to as PersonV0. The current Person structure is named PersonV1

    extension MigrationManager: MigrationManagerProtocol {
        func applyMigration() {
            defer { migrationPenging = false }
            applyPersonMigration()
        }
        
        private func isPersonMigrationPending() -> Bool {
            let userDefaultsManager = appSingletons.userDefaultsManager
            return userDefaultsManager.get(Person.self, forKey: UserDefaultsManager.key.person) == nil
        }
        
        private func applyPersonMigration() {
            let userDefaultsManager = appSingletons.userDefaultsManager
    
            guard isPersonMigrationPending() else {
                return // No migration needed
            }
            let currentStoredPersonVersion = storedPersonVersion()
            if currentStoredPersonVersion == 0,
                let personV0 = userDefaultsManager.get(PersonV0.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV1(name: personV0.name, age: personV0.age, email: "---")
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
        }
        
        private func storedPersonVersion() -> Int {
                return 0
        }
    ...
    }

    The migration process begins by determining the version of the Person structure stored using the storedPersonVersion() function. At this stage in the application’s evolution, the version is 0.

    If the current stored version is 0, the process involves fetching PersonV0 from UserDefaults and performing a migration. This migration entails transforming PersonV0 into PersonV1 by adding an email field with a default value.

    struct JSONPersistedMigrationApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .onAppear {
    #if DEBUG
                        setupDebugSwift()
    #endif
                    }
                    .onShake {
    #if DEBUG
                        DebugSwift.show()
    #endif
                    }
                    .task {
                        appSingletons.migrationManager.applyMigration()
                    }
            }
        }

    Finally, we call migrationManager.applyMigration() within the .task modifier to ensure it executes only once during the app’s startup.

    For debugging purposes, I found the DebugSwift tool very useful. I explain in more detail how to integrate this tool and its main features in the following post.

    Now, build and run the app:

    Migration is currently being executed, and the app is displaying data properly.

    When we open the DebugView tool to review user defaults, we observe that the migration has been completed exactly as expected.

    Did we really finish? Well, not yet. It is mandatory to implement a test case that ensures the Person object can be migrated from V0 to V1.

    Updating an attribute

    Once the infrastructure for running migrations is ready, the next changes to the persisted structure should involve either a Tic-Tac-Toe game or an easy recipe.

    Now, the business has requested that we rename the «name» field to «alias.» The first step is to update the structure by increasing the version number and renaming the field.

    struct Person: Codable {
        var version: Int {
            return 2
        }
        
        let alias: String
        let age: String
        let email: String
    }
    

    Second, add PersonV1 to the Migration Manager and set PersonV2 as the current Person structure.

    @MainActor
    final class MigrationManager: ObservableObject {
        @Published var migrationPenging = true
        @Published var migrationFailed = false
        
        struct PersonV0: Codable {
            let name: String
            let age: String
        }
        
        struct PersonV1: Codable {
            var version: Int {
                return 1
            }
            let name: String
            let age: String
            let email: String
        }
        
        typealias PersonV2 = Person
    }

    The third step is to update storedPersonVersion(). This time, the stored Person version could be either V0 or V1.

        private func storedPersonVersion() -> Int {
            let userDefaultsManager = appSingletons.userDefaultsManager
            if let personV1 = userDefaultsManager.get(PersonV1.self, forKey: UserDefaultsManager.key.person) {
                return 1
            } else {
                return 0
            }
        }

    The fourth step is to implement the migration-if-block inside applyPersonMigration.

        private func applyPersonMigration() {
            let userDefaultsManager = appSingletons.userDefaultsManager
    
            guard isPersonMigrationPending() else {
                return // No migration needed
            }
            let currentStoredPersonVersion = storedPersonVersion()
            if currentStoredPersonVersion == 0,
                let personV0 = userDefaultsManager.get(PersonV0.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV1(name: personV0.name, age: personV0.age, email: "---")
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
            if currentStoredPersonVersion <= 1,
                let personV1 = userDefaultsManager.get(PersonV1.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV2(alias: personV1.name, age: personV1.age, email: personV1.email)
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
        }

    It has to be an independent if-block,. In case the app were V0 both if-blocks would be executed, and in case were V1 the tha last block woud be executed.

    Fifth and last step, do unit test, now appears a new test case. One for testing migration from V0 to V2 and another for V1 to V2:

    Remember, five steps one after the other.

    Removing an attribute

    Now, we will remove the age attribute. One, update the person structure.

    struct Person: Codable {
        var version: Int {
            return 3
        }
        
        let alias: String
        let email: String
    }

    Two, add PersonV2 to MigrationManager and set PersonV3 as the current Person structure.

    @MainActor
    final class MigrationManager: ObservableObject {
        @Published var migrationPenging = true
        @Published var migrationFailed = false
        
        struct PersonV0: Codable {
            let name: String
            let age: String
        }
        
        struct PersonV1: Codable {
            var version: Int {
                return 1
            }
            
            let name: String
            let age: String
            let email: String
        }
        
        struct PersonV2: Codable {
            var version: Int {
                return 2
            }
            
            let alias: String
            let age: String
            let email: String
        }
        
        typealias PersonV3 = Person
    }

    Three, update storedPersonVersion(). This time, the stored Person version could be V0, V1, or V2:

        private func storedPersonVersion() -> Int {
            let userDefaultsManager = appSingletons.userDefaultsManager
            if let _ = userDefaultsManager.get(PersonV2.self, forKey: UserDefaultsManager.key.person) {
                return 2
            } else if let _ = userDefaultsManager.get(PersonV1.self, forKey: UserDefaultsManager.key.person) {
                return 1
            } else {
                return 0
            }
        }

    Four, the migration-if-block inside applyPersonMigration:

    private func applyPersonMigration() {
            let userDefaultsManager = appSingletons.userDefaultsManager
    
            guard isPersonMigrationPending() else {
                return // No migration needed
            }
            let currentStoredPersonVersion = storedPersonVersion()
            if currentStoredPersonVersion == 0,
                let personV0 = userDefaultsManager.get(PersonV0.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV1(name: personV0.name, age: personV0.age, email: "---")
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
            if currentStoredPersonVersion <= 1,
                let personV1 = userDefaultsManager.get(PersonV1.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV2(alias: personV1.name, age: personV1.age, email: personV1.email)
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
            if currentStoredPersonVersion <= 2,
                let personV2 = userDefaultsManager.get(PersonV2.self, forKey: UserDefaultsManager.key.person) {
                let person = PersonV3(alias: personV2.alias, email: personV2.email)
                saveInUserDefaults(person, UserDefaultsManager.key.person, &migrationFailed)
            }
        }

    Five: Perform unit tests. A new test case has now been added, which tests migration from V0 to V3, V1 to V3, and V2 to V3.

    Conclusions

    In this post, you learned how to migrate non-database persisted data in your app. You can find the base project for developing this post in this repository.