Categoría: Swift

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

  • Seamless Text Input with Your Voice on iOS

    Seamless Text Input with Your Voice on iOS

    Most likely, you have faced a situation where you’re enjoying the seamless flow of an application—for instance, while making a train or hotel reservation. Then, suddenly—bam!—a never-ending form appears, disrupting the experience. I’m not saying that filling out such forms is irrelevant for the business—quite the opposite. However, as an app owner, you may notice in your analytics a significant drop in user conversions at this stage.

    In this post, I want to introduce a more seamless and user-friendly text input option to improve the experience of filling out multiple fields in a form.

    Base project

    To help you understand this topic better, we’ll start with a video presentation. Next, we’ll analyze the key parts of the code. You can also download the complete code from the repository linked below.

    To begin entering text, long-press the desired text field. When the bottom line turns orange, it indicates that the has been activated speech-to-text mode. Release your finger once you see the text correctly transcribed. If the transcribed text is correct, the line will turn green; otherwise, it will turn red.

    Let’s dig in the code…

    The view is built with a language picker, which is a crucial feature. It allows you to select the language you will use later, especially when interacting with a form containing multiple text fields.

    struct VoiceRecorderView: View {
       @StateObject private var localeManager = appSingletons.localeManager
        @State var name: String = ""
        @State var surename: String = ""
        @State var age: String = ""
        @State var email: String = ""
        var body: some View {
            Form {
                Section {
                    Picker("Select language", selection: $localeManager.localeIdentifier) {
                        ForEach(localeManager.locales, id: \.self) { Text($0).tag($0) }
                    }
                    .pickerStyle(SegmentedPickerStyle())
                    .onChange(of: localeManager.localeIdentifier) {
                    }
                }
    
                Section {
                    TextFieldView(textInputValue: $name,
                                  placeholder: "Name:",
                                  invalidFormatMessage: "Text must be greater than 6 characters!") { textInputValue in
                        textInputValue.count > 6
                    }
                    
                    TextFieldView(textInputValue: $surename,
                                  placeholder: "Surename:",
                                  invalidFormatMessage: "Text must be greater than 6 characters!") { textInputValue in
                        textInputValue.count > 6
                    }
                    TextFieldView(textInputValue: $age,
                                  placeholder: "Age:",
                                  invalidFormatMessage: "Age must be between 18 and 65") { textInputValue in
                        if let number = Int(textInputValue) {
                            return number >= 18 && number <= 65
                        }
                        return false
                    }
                }
                
                Section {
                    TextFieldView(textInputValue: $email,
                                  placeholder: "Email:",
                                  invalidFormatMessage: "Must be a valid email address") { textInputValue in
                        let emailRegex = #"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#
                        let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
                        return emailPredicate.evaluate(with: textInputValue)
                    }
                }   
            }
            .padding()
        }
    }

    For every text field, we need a binding variable to hold the text field’s value, a placeholder for guidance, and an error message to display when the acceptance criteria function is not satisfied.

    When we examine the TextFieldView, we see that it is essentially a text field enhanced with additional features to improve user-friendliness.

    struct TextFieldView: View {
        
        @State private var isPressed = false
        
        @State private var borderColor = Color.gray
        @StateObject private var localeManager = appSingletons.localeManager
    
        @Binding var textInputValue: String
        let placeholder: String
        let invalidFormatMessage: String?
        var isValid: (String) -> Bool = { _ in true }
        
        var body: some View {
            VStack(alignment: .leading) {
                if !textInputValue.isEmpty {
                    Text(placeholder)
                        .font(.caption)
                }
                TextField(placeholder, text: $textInputValue)
                    .accessibleTextField(text: $textInputValue, isPressed: $isPressed)
                    .overlay(
                        Rectangle()
                            .frame(height: 2)
                            .foregroundColor(borderColor),
                        alignment: .bottom
                    )
                .onChange(of: textInputValue) { oldValue, newValue in
                        borderColor = getColor(text: newValue, isPressed: isPressed )
                }
                .onChange(of: isPressed) {
                        borderColor = getColor(text: textInputValue, isPressed: isPressed )
                }
                if !textInputValue.isEmpty,
                   !isValid(textInputValue),
                    let invalidFormatMessage {
                    Text(invalidFormatMessage)
                        .foregroundColor(Color.red)
                }
            }
        }
        
        func getColor(text: String, isPressed: Bool) -> Color {
            guard !isPressed else { return Color.orange }
            guard !text.isEmpty else { return Color.gray }
            return isValid(text) ? Color.green : Color.red
        }
        
    }

    The key point in the above code is the modifier .accessibleTextField, where all the magic of converting voice to text happens. We have encapsulated all speech-to-text functionality within this modifier.

    extension View {
        func accessibleTextField(text: Binding<String>, isPressed: Binding<Bool>) -> some View {
            self.modifier(AccessibleTextField(text: text, isPressed: isPressed))
        }
    }
    
    struct AccessibleTextField: ViewModifier {
        @StateObject private var viewModel = VoiceRecorderViewModel()
        
        @Binding var text: String
        @Binding var isPressed: Bool
        private let lock = NSLock()
        func body(content: Content) -> some View {
            content
                .onChange(of: viewModel.transcribedText) {
                    guard viewModel.transcribedText != "" else { return }
                    self.text = viewModel.transcribedText
                }
                .simultaneousGesture(
                    DragGesture(minimumDistance: 0)
                        .onChanged { _ in
                            lock.withLock {
                                if !isPressed {
                                    isPressed = true
                                    viewModel.startRecording(locale: appSingletons.localeManager.getCurrentLocale())
                                }
                            }
                            
                        }
                        .onEnded { _ in
                            
                            if isPressed {
                                lock.withLock {
                                    isPressed = false
                                    viewModel.stopRecording()
                                }
                            }
                        }
                )
        }
    }

    The voice-to-text functionality is implemented in the VoiceRecorderViewModel. In the view, it is controlled by detecting a long press from the user to start recording and releasing to stop the recording. The transcribed voice text is then forwarded upward via the text Binding attribute.

    Finally, here is the view model that handles the transcription:

    import Foundation
    import AVFoundation
    import Speech
    
    class VoiceRecorderViewModel: ObservableObject {
        @Published var transcribedText: String = ""
        @Published var isRecording: Bool = false
        
        private var audioRecorder: AVAudioRecorder?
        private let audioSession = AVAudioSession.sharedInstance()
        private let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        private var recognitionTask: SFSpeechRecognitionTask?
        private var audioEngine = AVAudioEngine()
        
        var speechRecognizer: SFSpeechRecognizer?
    
        func startRecording(locale: Locale) {
            do {
                self.speechRecognizer = SFSpeechRecognizer(locale: locale)
    
                recognitionTask?.cancel()
                recognitionTask = nil
    
                try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
                try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
    
                guard let recognizer = speechRecognizer, recognizer.isAvailable else {
                    transcribedText = "Reconocimiento de voz no disponible para el idioma seleccionado."
                    return
                }
                
                let inputNode = audioEngine.inputNode
                let recordingFormat = inputNode.outputFormat(forBus: 0)
                inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, when in
                    self.recognitionRequest.append(buffer)
                }
                
                audioEngine.prepare()
                try audioEngine.start()
                
                recognitionTask = recognizer.recognitionTask(with: recognitionRequest) { result, error in
                    if let result = result {
                        self.transcribedText = result.bestTranscription.formattedString
                    }
                }
                
                isRecording = true
            } catch {
                transcribedText = "Error al iniciar la grabación: \(error.localizedDescription)"
            }
        }
        
        func stopRecording() {
            audioEngine.stop()
            audioEngine.inputNode.removeTap(onBus: 0)
            recognitionRequest.endAudio()
            recognitionTask?.cancel()
            isRecording = false
        }
    }

    Key Components

    1. Properties:

      • @Published var transcribedText: Holds the real-time transcribed text, allowing SwiftUI views to bind and update dynamically.
      • @Published var isRecording: Indicates whether the application is currently recording.
      • audioRecorder, audioSession, recognitionRequest, recognitionTask, audioEngine, speechRecognizer: These manage audio recording and speech recognition.
    2. Speech Recognition Workflow:

      • SFSpeechRecognizer: Recognizes and transcribes speech from audio input for a specified locale.
      • SFSpeechAudioBufferRecognitionRequest: Provides an audio buffer for speech recognition tasks.
      • AVAudioEngine: Captures microphone input.

    Conclusions

    I aim you that you download the project  from following github repositoryand start to play with such great techology.

    References

    • Speech

      Apple Developer Documentation

  • iOS start up sequencer pattern

    iOS start up sequencer pattern

    In mobile native apps (iOS/Android), it is quite common to execute a series of tasks before the app is ready for the user. These tasks might include checking if the app requires an update, fetching remote app configurations, presenting the «What’s New» information for the latest release, and requesting user login if the user is not already logged in. All of this needs to be done as quickly as possible, often with animations playing to keep the user engaged during the wait.

    This post introduces what I call the sequencer pattern. By leveraging NSOperation, we can encapsulate each task into a self-contained unit and define dependencies among them. This approach establishes the initial execution order of the tasks. An added advantage is that when two or more tasks have no dependencies, iOS can execute them in parallel, further reducing startup times.

    Adding splash screen

    The first task we will add is responsible for presenting the splash screen. First, we will modify the ContentView.

    struct ContentView: View {
        @StateObject var sequencer = appSingletons.sequencer
        var body: some View {
            if sequencer.isDone {
                HomeView()
            } else {
                sequencer.currentView
            }
        }    
    }

    Sequencer at the end is another singleton, but gathered in a global structure. I explain the benefits of this aproach in the post Safely gathering singletons while avoiding data races. And then basically while sqeuencer has not finished (!sequencer.isDone) is the responsible for providing view depending on task executed. When is done then is delegated whole view hierarchy to HomeView.

    Let’s see what is on Sequencer:

    final class Sequencer: ObservableObject {
        @MainActor
        @Published var isDone: Bool = false
    
        @MainActor
        @Published var currentView: AnyView = AnyView(Text("Initial View"))
    
        @MainActor
        func updateView(to newView: AnyView) {
            currentView = newView
        }
    
        @MainActor
        static let shared = Sequencer()
    
        fileprivate let operationQueue = OperationQueue()
    
        private init() { }
    
        @MainActor
        func start() {
            Task {
                await self.regularInitialSequence()
            }
        }
    
        @GlobalManager
        func regularInitialSequence() {
            let presentSplashOperation = PresentSplashOperation()
            let operations = [presentSplashOperation]
            
            // Add operation dependencies
    
            operationQueue.addOperations(operations, waitUntilFinished: false)
        }
    
        func cancel() {
            operationQueue.cancelAllOperations()
        }
    }

    The Sequencer is an ObservableObject that publishes the current view associated with any task, as well as its readiness status. The start method creates tasks and initiates their execution. Currently, only the splash view task is being executed.

    The PresentSplashOperation performs the following functions:

        override func main() {
            os_log("Start: PresentSplashOperation", log: log, type: .debug)
            Task { @MainActor in
                Sequencer.shared.updateView(to: AnyView(SequencerView()))
            }
            sleep(5)
            os_log("End: PresentSplashOperation", log: log, type: .debug)
            self.state = .Finished
            Task { @MainActor in
                Sequencer.shared.isDone = true
            }
        }

    Provides the view to be displayed while the PresentingSplashOperation is being executed. Afterward, there is a delay of 5 seconds before marking the task as finished. Once completed:

    1. isDone is set to true, allowing view control to transition to ContentView and present HomeView.
    2. self.state is set to .Finish, enabling the NSOperations engine to execute the next task, if another operation depends on this one to start.

    To initiate the process, simply call the start method from the sequencer to begin the sequence.

    @main
    struct SequencerPatternApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .task {
                    appSingletons.sequencer.start()
                }
            }
        }
    }

    Build and run on a simulator or real device, and the result should be:

    What’s new screen

    It is quite common that whenever there is an app software update introducing new user features, a page viewer is displayed once to showcase what’s new in the app. First of all, let’s set the app version in a centralized location, as explained in Force update iOS Apps when… post:

    Then we are going to implement a task that carries on this task:

    @GlobalManager
    final class WhatsNewOperation: ConcurrentOperation, @unchecked Sendable {
        
        
        override init() {
            super.init()
        }
    
        @MainActor
        func WhatsNewView() -> some View {
            VStack {
                HStack {
                    Spacer()
                    Button {
                        Sequencer.shared.isDone = true
                        self.state = .Finished
                    } label: {
                        Image(systemName: "xmark")
                            .font(.system(size: 20, weight: .bold))
                            .foregroundColor(.white)
                            .frame(width: 40, height: 40)
                            .background(Color.red)
                            .clipShape(Circle())
                            .shadow(radius: 5)
                    }
                }
               // Spacer()
                TabView{
                    VStack {
                        Text("What's new feature A")
                    }
                    VStack {
                        Text("What's new feature B")
                    }
                    VStack {
                        Text("What's new feature C")
                    }
                }
                .font(.system(size: 20, weight: .bold))
                .tabViewStyle(.page)
                .indexViewStyle(.page(backgroundDisplayMode: .always))
            }
            .padding()
        }
        
        override func main() {
            @AppStorage("appVersion") var appVersion = "0.0.0"
            
            os_log("Start: WhatsNewOperation", log: log, type: .debug)
            let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
            let isLatest = appVersion == marketingVersion
            if !isLatest {
                appVersion = marketingVersion
                Task { @MainActor in
                    Sequencer.shared.updateView(to: AnyView(WhatsNewView()))
                }
            } else {
                self.state = .Finished
                Task { @MainActor in
                    Sequencer.shared.isDone = true
                }
            }
            os_log("End: WhatsNewOperation", log: log, type: .debug)
        }
    }
     

    We fetch the appVersion from the App Store (via UserDefaults, as implemented previously). This is compared against the current app version stored in the project configuration (MARKETING_VERSION). If the versions differ, a «What’s New» view is presented. After this, the new MARKETING_VERSION value is stored in AppStorage.

    An important note: the last task in the sequencer is now WhatsNewOperation. As a result, this operation is responsible for setting Sequencer.shared.isDone to true. PresentSplashOperation is no longer responsible for setting this flag. Be sure to remove any code in PresentSplashOperation that sets this flag; otherwise, HomeView will be presented as soon as PresentSplashOperation finishes.

        override func main() {
            os_log("Start: PresentSplashOperation", log: log, type: .debug)
            Task { @MainActor in
                Sequencer.shared.updateView(to: AnyView(SequencerView()))
            }
            sleep(5)
            os_log("End: PresentSplashOperation", log: log, type: .debug)
            self.state = .Finished
    //        Task { @MainActor in
    //            Sequencer.shared.isDone = true
    //        }
        }

    Look out! The self.state = .Finished remains untouched. Now, this will allow the NSOperation engine to process the next operation (PresentSplashOperation). It is now time to create a new operation, set its dependencies, and update the regularInitialSequence() method.

        @GlobalManager
        func regularInitialSequence() {
            let presentSplashOperation = PresentSplashOperation()
            let whatsNewOperation = WhatsNewOperation()
            
            // DO NOT FORGET ADD OPERATION IN operations array. XDDDDD
            let operations = [presentSplashOperation,
                              whatsNewOperation]
            
            // Add operation dependencies
            whatsNewOperation.addDependency(presentSplashOperation)
            
            operationQueue.addOperations(operations, waitUntilFinished: false)
        }

    Add a new WhatsNewOperation() to the operations array. It’s important to set the whatsNewOperation to depend on the completion of the presentSplashOperation.

    Build and run. The expected result should be:

    The ‘What’s New’ section is displayed only once upon the app’s first startup and does not appear on subsequent launches.

    Force update

    We are now going to insert a force update operation between the previous steps. Specifically, the sequence will be: PresentSplash, followed by ForceUpdateOperation, and then What’s New.

    The implementation of the force update operation is as follows:

    @GlobalManager
    final class ForceUpdateOperation: ConcurrentOperation, @unchecked Sendable {
    
        override init() {
            super.init()
        }
    
        @MainActor
        func ForceUpdateRequiredView() -> some View {
            VStack {
              //  ProgressView()
                Text("Software Update Required!!!")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                Button("Download it from Apple Store...") {}
                    .buttonStyle(.borderedProminent)
            }
                .padding()
        }
    
        override func main() {
            let required = true
            os_log("Start: ForceUpdateOperation", log: log, type: .debug)
            sleep(5)
            if required {
                Task { @MainActor in
                    Sequencer.shared.updateView(to: AnyView(ForceUpdateRequiredView()))
                }
            } else {
                self.state = .Finished
            }
            os_log("End: ForceUpdateOperation", log: log, type: .debug)
        }
    }

    To emulate behavior, we initially included a 5-second sleep. Using a hardcoded flag, we determined whether a force update was required.

    Now that we have a long-running operation, we can remove the sleep delay in the PresentSplash operation.

        override func main() {
            os_log("Start: PresentSplashOperation", log: log, type: .debug)
            Task { @MainActor in
                Sequencer.shared.updateView(to: AnyView(SequencerView()))
            }
    //        sleep(5)
            os_log("End: PresentSplashOperation", log: log, type: .debug)
            self.state = .Finished
    //        Task { @MainActor in
    //            Sequencer.shared.isDone = true
    //        }
        }
    }

    It’s time to reorganize operations. Open the Sequencer and update the regularInitialSequence method.

        @GlobalManager
        func regularInitialSequence() {
            let presentSplashOperation = PresentSplashOperation()
            let forceUpdateOperation = ForceUpdateOperation()
            let whatsNewOperation = WhatsNewOperation()
            
            // DO NOT FORGET ADD OPERATION IN operations array. XDDDDD
            let operations = [presentSplashOperation,
                              forceUpdateOperation,
                              whatsNewOperation]
            
            // Add operation dependencies
            forceUpdateOperation.addDependency(presentSplashOperation)
            whatsNewOperation.addDependency(forceUpdateOperation)
            
            operationQueue.addOperations(operations, waitUntilFinished: false)
        }

    Simply add a new ForceUpdateOperation to the operations array and reorganize the operation dependencies. The WhatsNewOperation should depend on the ForceUpdateOperation, and the ForceUpdateOperation should depend on the PresentSplashOperation.

    After making these changes, build and run the application.

    I will now set the required flag in the ForceUpdateOperation to false, so it doesn’t block the app startup sequence. We will then review the logs to assess the execution sequence of the operations.

    Fetch configuration

    Up until now, we have been sequencing operations, but sometimes it is possible to parallelize operations to reduce startup sequence time. In this case, we have created a simulated FetchConfiguration operation:

    @GlobalManager
    final class FetchConfigurationOperation: ConcurrentOperation, @unchecked Sendable {
    
        override init() {
            super.init()
        }
    
        override func main() {
            let required = true
            os_log("Start: FetchConfigurationOperation", log: log, type: .debug)
            sleep(8)
            self.state = .Finished
            os_log("End: FetchConfigurationOperation", log: log, type: .debug)
        }
    }

    To emulate the behavior, we initially included an 8-second sleep. This operation will be executed in parallel with the Force Update operation. Let’s create the operation and add its dependencies.

        @GlobalManager
        func regularInitialSequence() {
            let presentSplashOperation = PresentSplashOperation()
            let forceUpdateOperation = ForceUpdateOperation()
            let fetchConfigurationOperation = FetchConfigurationOperation()
            let whatsNewOperation = WhatsNewOperation()
            let operations = [presentSplashOperation,
                              forceUpdateOperation,
                              fetchConfigurationOperation,
                              whatsNewOperation]
            
            // Add operation dependencies
            forceUpdateOperation.addDependency(presentSplashOperation)
            
            fetchConfigurationOperation.addDependency(presentSplashOperation)
            
            whatsNewOperation.addDependency(forceUpdateOperation)
            whatsNewOperation.addDependency(fetchConfigurationOperation)
    
            operationQueue.addOperations(operations, waitUntilFinished: false)
        }

    Create a FetchConfigurationOperation and add it to the operations array. Ensure that it has the same dependencies as the ForceUpdateConfiguration operation. Additionally, the WhatsNewOperation must also depend on FetchConfiguration.

    Build the project, run the operation, and review the logs.

    ForceUpdateOperation and FetchConfiguration start simultaneously, and it is only when FetchConfiguration finishes that the WhatsNewOperation is executed.

    Conclusions

    I have used this pattern in two real production iOS projects with reasonable success, and I encourage you to try it out in your personal projects. You can find the codebase used to write this post in this repository.

  • Force Update iOS Apps When Backend Require It

    Force Update iOS Apps When Backend Require It

    In the mobile native (iOS/Android) app production ecosystem, multiple frontend versions often coexist and interact with the same backend. Frontend updates are typically adopted gradually; while it’s possible to enforce an update, this approach is generally considered disruptive and is used only in exceptional circumstances.

    This post aims to demonstrate a method for controlling request responses based on the frontend version specified in the request. The backend implementation will use Vapor, and the frontend will be an iOS app. Links to the GitHub repositories hosting the source code are provided at the end of this post.

    Keep request under control

    Including the client’s frontend version in backend requests is crucial for several reasons:

    1. Version-Specific Responses: The backend can tailor its responses to ensure compatibility and optimal functionality for each frontend version.

    2. API Versioning: It helps the backend serve the appropriate API version, supporting backward compatibility while enabling updates and improvements.

    3. Feature Support: Frontend versions may differ in their feature sets. The backend can adjust responses to include or exclude functionality based on the client’s capabilities.

    4. Performance Optimization: Backend processing and payloads can be optimized for the specific requirements of each frontend version, improving system performance.

    5. Error Handling: Knowing the frontend version allows for more relevant error messages and effective resolution of version-specific issues.

    6. Security Enhancements: Version-specific security protocols or restrictions can be implemented, boosting system security.

    By including the frontend version in client requests, developers can build robust, efficient, and maintainable systems that adapt to evolving requirements while maintaining compatibility with legacy clients.

    Vapor backend

    Vapor is an open-source web framework written in Swift, designed for building server-side applications. It offers a powerful and asynchronous platform for developing web applications, APIs, and backend services, all using Swift as the server-side language.

    This post is not a «build your first server-side app» tutorial. However, don’t worry—at the end of the post, I’ll share the tutorials I followed to gain a deeper understanding of this technology.

    To get started, we’ll create a new Vapor project. For this project, we won’t be working with databases, so you can safely answer «No» to all related prompts during the setup process.

    We will create an endpoint specifically for checking the minimum required versions compatible with the backend and determining whether a forced update is necessary. The endpoint will use the GET method, and the path will be /minversion.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)

    And the associated function to perform this will be as follows.

        @Sendable
        func minVersion(req: Request) async throws -> VersionResponse {
            
            let currentVersion = "2.0.0"
            let minimumVersion = "1.5.0"
            let forceUpdate = true // o false dependiendo de la lógica de negocio
    
            // Devuelve la respuesta como JSON
            return VersionResponse(
                currentVersion: currentVersion,
                minimumVersion: minimumVersion,
                forceUpdate: forceUpdate
            )
        }

    Structure: We need to include the following information:

    1. Minimal Version: The minimum version of the application that the backend can handle.
    2. Current Version: The current version supported by the backend.
    3. Force Update: Whether a forced update is required.

    Instructions:
    Run the project, and check the log console to confirm that the server is ready.

    Use the curl command to call the specified endpoint.

    The API returns a JSON object containing the minimum and current versions, as well as a force-update flag.

    To simplify the backend’s ability to check frontend versions, we will add an additional attribute to each endpoint. This attribute will provide information about the frontend version. To illustrate this approach, we will create a sample POST endpoint that includes this feature.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)
            
            let sampleRoutesGrouped = routes.grouped("sample")
            sampleRoutesGrouped.post(use: sample)
        }
    And its functionality is encapsulated in the following endpoint.
        @Sendable
        func sample(req: Request) async throws -> SampleResponse {
            let payload = try req.content.decode(SampleRequestData.self)
            let isLatestVersion =  await payload.version == VersionResponse.current().currentVersion
            let isForceUpdate = await VersionResponse.current().forceUpdate
            guard  isLatestVersion ||
                   !isForceUpdate else {
                throw Abort(.upgradeRequired) // Force update flag set
            }
    
            guard await isVersion(payload.version, inRange: (VersionResponse.current().minimumVersion, VersionResponse.current().currentVersion)) else {
                throw Abort(.upgradeRequired) // Version out of valid range
            }
            
            return SampleResponse(data: "Some data...")
        }

    The first thing the function does is validate that the version adheres to the X.Y.Z syntax.

        struct SampleRequestData: Content {
            let version: String
            
            mutating func afterDecode() throws {
                guard isValidVersionString(version) else {
                    throw Abort(.badRequest, reason: "Wrong version format")
                }
            }
            
            private func isValidVersionString(_ version: String) -> Bool {
                let versionRegex = #"^\d+\.\d+\.\d+$"#
                let predicate = NSPredicate(format: "SELF MATCHES %@", versionRegex)
                return predicate.evaluate(with: version)
            }
        }

    Later on, the process involves validating the version of a client application against a server-defined versioning policy. If the version check is successful, a simple JSON response with sample data is returned.

    Returning to the command line, we execute the sample using valid version values:

    We received a valid sample endpoint response, along with current, minimum version and wether forced update is being required.

    However, when we set a version lower than the required minimum, we encountered an error requesting an upgrade.

    While the implementation is theoretically complete, handling version updates on the front end is not difficult, but any mistakes in production can have dramatic consequences. For this reason, it is mandatory to implement a comprehensive set of unit tests to cover the implementation and ensure that when versions are updated, consistency is maintained.

    From now on, every new endpoint implemented by the server must perform this frontend version check, along with other checks, before proceeding. Additionally, the code must be data race-safe.

    At the time of writing this post, I encountered several issues while compiling the required libraries for Vapor. As a result, I had to revert these settings to continue writing this post. Apologies for the back-and-forth.

    IOS frontend

    The iOS app frontend we are developing will primarily interact with a sample POST API. This API accepts JSON data, which includes the current frontend version.

    • If the frontend version is within the supported range, the backend responds with the expected output for the sample POST API, along with information about the versions supported by the backend.
    • If the frontend version falls below the minimum supported version and a forced update is required, the backend will return an «update required» error response.

    To ensure compliance with Swift 6, make sure that Strict Concurrency Checking is set to Complete.

    … and Swift language version to Swift 6.

    Before we start coding, let’s set the app version. The version can be defined in many places, which can be quite confusing. Our goal is to set it in a single, consistent location.

    This is the unique place where you need to set the version number. For the rest of the target, we will inherit that value. When we set the version in the target, a default value (1.0) is already set, and it is completely isolated from the project. We are going to override this by setting MARKETING_VERSION to $(MARKETING_VERSION), so the value will be taken from the project’s MARKETING_VERSION.

    Once set, you will see that the value is adopted. One ring to rule them all.

    The application is not very complicated, and if you’re looking for implementation details, you can find the GitHub repository at the end of the post. Essentially, what it does is perform a sample request as soon as the view is shown.

    Make sure the Vapor server is running before launching the app on a simulator (not a real device, as you’re targeting localhost). You should see something like this:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.05.23

    The current app version is 1.7.0, while the minimum supported backend version is 1.5.0, and the backend is currently at version 2.0.0. No forced update is required. Therefore, the UI displays a message informing users that they are within the supported version range, but it also indicates that an update to the latest version is available.

    Once we configure the Vapor backend to enforce a forced update:

            let versionResponse = VersionResponse(currentVersion: "2.0.0",
                                                  minimumVersion: "1.5.0",
                                                  forceUpdate: true)
            

    Re-run vapor server:

    Screenshot

    Re-run the app:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.16.03

    The front-end needs to be updated, and users are required to update the app. Please provide a link to the Apple Store page for downloading the update.

    Conclusions

    In this post, I have demonstrated a method for versioning API communication between the backend and frontend. I acknowledge that my explanation of the implementation is brief, but you can find the backend and frontend repositories linked here.

    References

  • Safely Gathering Singletons While Avoiding Data Races

    Safely Gathering Singletons While Avoiding Data Races

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

    Base project

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

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

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

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

    Key Concepts at Work

    1. Actor Isolation:

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

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

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

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

    Key Concepts at Work

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

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

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

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

    All the Singletons came together at one location

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

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

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

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

    Ensure that singleton references are obtained from appSinglegons.

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

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

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

    …Thread safe touch

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

    … and Swift language version to Swift 6.

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

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

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

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

    Conclusions

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

    References

  • iOS Location Managers: A Thread-Safe Approach

    iOS Location Managers: A Thread-Safe Approach

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

    CoreLocation

    Core Location Framework Overview

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

    Key Functionalities of Core Location:

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

    iOS Location sample app:

    Create a new blank iOS SwiftUI APP.

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

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

    Main Responsabilites and features:

    1. Singleton Pattern

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

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

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

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

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

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

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

    How it Works in Steps

    1. Initialization

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

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

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

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

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

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

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

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

    …Thread safe approach

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

    … and Swift language version to Swift 6.

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

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

    @MainActor
    class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
     

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

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

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

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

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

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

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

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

    Conclusions

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

    References

  • Writing a Barcode Reader App in No Time

    Writing a Barcode Reader App in No Time

    There are other ways to input data into your application besides using a device’s keyboard. One such method is reading barcodes. In this post, I’ll demonstrate how easy it is to implement a solution for this functionality.

    AVCaptureMetadataOutput

    AVCaptureMetadataOutput is the class responsible for intercepting metadata objects from the video stream captured during a session. Part of the AVFoundation framework, its primary purpose is to detect and process metadata in real-time while capturing video.

    Key Characteristics of AVCaptureMetadataOutput:
    1. Code Detection:
      This class can detect various types of codes, such as QR codes and barcodes, including formats like EAN-8, EAN-13, UPC-E, Code39, and Code128, among others.

    2. Flexible Configuration:
      You can specify the types of metadata you want to capture using the metadataObjectTypes property. This provides granular control over the kind of information the system processes.

    3. Delegate-Based Processing:
      Metadata detection and processing are managed via a delegate object. This approach provides flexibility in handling the detected data and enables custom responses. However, note that working with this delegate often requires integration with the UIKit framework for user interface handling.

    4. Integration with AVCaptureSession:
      The AVCaptureMetadataOutput instance is added as an output to an AVCaptureSession. This setup enables real-time processing of video data as it is captured.

    Creating iOS App sample app

    Create a new blank iOS SwiftUI APP, and do not forget set Strict Concurrency Checking to Complete and Swift Language Versionto Swift 6

    As I mention on point 3 from past section, the pattern that implements AVCaptureMetadataOutput is deletage patterns, but we want our app that uses the latest and coolest SwiftUI framework. For fixing that we will need support of our old friend UIKit. Basically wrap UIKit ViewController into a UIViewControllerRespresentable, for being accessible from SwiftUI. And finally implement delegate inside UIViewControllerRespresentable.

    Create a new file called ScannerPreview and start writing following code:

    import SwiftUI
    import AVFoundation
    
    // 1
    struct ScannerPreview: UIViewControllerRepresentable {
        @Binding var isScanning: Bool
        var didFindBarcode: (String) -> Void = { _ in }
        // 2
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
        // 3
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            // Setup the camera input
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            // Setup the metadata output
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.ean13, .ean8, .pdf417, .upce, .qr, .aztec] // Add other types if needed
            } else {
                return viewController
            }
    
            // Setup preview layer
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.layer.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            captureSession.startRunning()
    
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
    }

    To integrate a UIViewController into a SwiftUI View, import SwiftUI (for access to UIViewControllerRepresentable) and AVFoundation (for AVCaptureMetadataOutputObjectsDelegate).

    Key Features and Implementation
      1. UIViewControllerRepresentable Protocol
        Implementing the UIViewControllerRepresentable protocol allows a UIKit UIViewController to be reused within SwiftUI.

        • isScanning: This is a binding to the parent view, controlling the scanning state.
        • didFindBarcode: A callback function that is executed whenever a barcode is successfully scanned and read.
      2. Coordinator and Bridging

        • makeCoordinator: This method is required to fulfill the UIViewControllerRepresentable protocol. It creates a «bridge» (e.g., a broker, intermediary, or proxy) between the UIKit UIViewController and the SwiftUI environment. In this implementation, the Coordinator class conforms to the AVCaptureMetadataOutputObjectsDelegate protocol, which handles metadata detection and processing.
      3. Creating the UIViewController

        • makeUIViewController: Another required method in the protocol, responsible for returning a configured UIViewController.
          • Inside this method, the AVCaptureSession is set up to detect specific barcode formats (e.g., EAN-13, EAN-8, PDF417, etc.).
          • The configured session is added as a layer to the UIViewController.view.
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
        
        //1
        @MainActor
        class Coordinator: NSObject, @preconcurrency AVCaptureMetadataOutputObjectsDelegate {
            var parent: ScannerPreview
            
            init(parent: ScannerPreview) {
                self.parent = parent
            }
            // 2
            // MARK :- AVCaptureMetadataOutputObjectsDelegate
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                // 4
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    self.parent.isScanning = false
                    // 3
                    parent.didFindBarcode(String(stringValue))
                }
            }
        }

    Later, we will implement the Coordinator class, which must inherit from NSObject because it needs to conform to the AVCaptureMetadataOutputObjectsDelegate protocol, an extension of NSObjectProtocol.

    Key Features and Implementation:
    1. Swift 6 Compliance and Data Race Avoidance
      To ensure compliance with Swift 6 and avoid data races, the class is executed on @MainActor. This is necessary because it interacts with attributes from its parent, UIViewControllerRepresentable. Since AVCaptureMetadataOutput operates in a non-isolated domain, we’ve marked the class with @MainActor.

    2. Thread Safety
      Before marking AVCaptureMetadataOutputObjectsDelegate with @preconcurrency, ensure the following:

      • The metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) call is executed on the main thread (@MainActor).
      • This guarantees that when setting up AVCaptureMetadataOutput, it operates safely on the main thread.
    3. Data Handling
      The parent view receives a copy of the scanned barcode string. At no point does the delegate implementation modify the received data. This ensures thread safety and avoids potential data races.

    4. Protocol Method Implementation
      In the protocol method implementation:

      • Fetch the first object.
      • Retrieve the barcode value.
      • Update the scanning state.
      • Execute the callback function.

    By ensuring that no data is modified across different isolated domains, it is safe to proceed with marking the protocol with @preconcurrency.

     

    Final step is just implent the SwiftUI view where ScannerPreview view will be embeded. Create a new file called BarcodeScannerView and write following code:

    import SwiftUI
    import AVFoundation
    
    struct BarcodeScannerView: View {
        @State private var scannedCode: String?
        @State private var isScanning = true
        @State private var showAlert = false
        
        var body: some View {
            VStack {
                Text("Scan a Barcode")
                    .font(.largeTitle)
                    .padding()
    
                ZStack {
                    //1
                    ScannerPreview(isScanning: $isScanning,
                                   didFindBarcode: { value in
                        scannedCode = value
                        showAlert = true
                    }).edgesIgnoringSafeArea(.all)
    
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            if let scannedCode = scannedCode {
                                Text("Scanned Code: \(scannedCode)")
                                    .font(.title)
                                    .foregroundColor(.white)
                                    .padding()
                            }
                            Spacer()
                        }
                        Spacer()
                    }
                }
    
                if !isScanning {
                    Button("Start Scanning Again") {
                        self.isScanning = true
                        self.scannedCode = nil
                    }
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
                }
            }
            .onAppear {
                self.scannedCode = nil
                self.isScanning = true
            }
        }
    }
    Key Features and Implementation:
    1. Just place the preview in a ZStack and implment the callback to execute when the barcode is read.

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            BarcodeScannerView()
        }
    }
    
    #Preview {
        ContentView()
    }
    

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

    Build and Run on real device

    For executing the app be sure that you deploy on a real device (iPhone or iPad). Whem the app ask you permission for using the camera, obviously say allow.

    Conclusions

    In this post, you have seen how easy it is to implement a barcode scanner using native libraries. You can find the working code used in this post in the following repository.

    References