Etiqueta: Swift

  • S.O.L.I.D. principles in Swift

    S.O.L.I.D. principles in Swift

    Applying SOLID principles to Swift is valuable because it enhances code quality, maintainability, and scalability while leveraging Swift’s unique features like protocols, value types, and strong type safety. Swift’s modern approach to object-oriented and protocol-oriented programming aligns well with SOLID, making it essential for developers aiming to write modular, testable, and flexible code.

    In this post we will revieew 5 principles by examples.

    S.O.L.I.D. principles

    The SOLID principles are a set of five design guidelines that help developers create more maintainable, flexible, and scalable software. These principles were introduced by Robert C. Martin, also known as Uncle Bob, and are widely adopted in object-oriented programming. The acronym SOLID stands for: Single Responsibility Principle (SRP)Open/Closed Principle (OCP)Liskov Substitution Principle (LSP)Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). Each principle addresses a specific aspect of software design, such as ensuring that a class has only one reason to change (SRP), allowing systems to be extended without modifying existing code (OCP), ensuring derived classes can substitute their base classes without altering behavior (LSP), creating smaller, more specific interfaces instead of large, general ones (ISP), and depending on abstractions rather than concrete implementations (DIP).

    By adhering to the SOLID principles, developers can reduce code complexity, improve readability, and make systems easier to test, debug, and extend. For example, SRP encourages breaking down large classes into smaller, more focused ones, which simplifies maintenance. OCP promotes designing systems that can evolve over time without requiring extensive rewrites. LSP ensures that inheritance hierarchies are robust and predictable, while ISP prevents classes from being burdened with unnecessary dependencies. Finally, DIP fosters loose coupling, making systems more modular and adaptable to change. Together, these principles provide a strong foundation for writing clean, efficient, and sustainable code.

    SRP-Single responsability principle

    The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented design, stating that a class or module should have only one reason to change, meaning it should have only one responsibility or job. This principle emphasizes that each component of a system should focus on a single functionality, making the code easier to understand, maintain, and test. By isolating responsibilities, changes to one part of the system are less likely to affect others, reducing the risk of unintended side effects and improving overall system stability. In essence, SRP promotes modularity and separation of concerns, ensuring that each class or module is cohesive and focused on a specific task.

    // Violating SRP
    class EmployeeNonSRP {
        let name: String
        let position: String
        let salary: Double
        
        init(name: String, position: String, salary: Double) {
            self.name = name
            self.position = position
            self.salary = salary
        }
        
        func calculateTax() -> Double {
            // Tax calculation logic
            return salary * 0.2
        }
        
        func saveToDatabase() {
            // Database saving logic
            print("Saving employee to database")
        }
        
        func generateReport() -> String {
            // Report generation logic
            return "Employee Report for \(name)"
        }
    }
    
    // Adhering to SRP
    class Employee {
        let name: String
        let position: String
        let salary: Double
        
        init(name: String, position: String, salary: Double) {
            self.name = name
            self.position = position
            self.salary = salary
        }
    }
    
    class TaxCalculator {
        func calculateTax(for employee: Employee) -> Double {
            return employee.salary * 0.2
        }
    }
    
    class EmployeeDatabase {
        func save(_ employee: Employee) {
            print("Saving employee to database")
        }
    }
    
    class ReportGenerator {
        func generateReport(for employee: Employee) -> String {
            return "Employee Report for \(employee.name)"
        }
    }

    In the first example, the Employee class violates SRP by handling multiple responsibilities: storing employee data, calculating taxes, saving to a database, and generating reports.

    The second example adheres to SRP by separating these responsibilities into distinct classes. Each class now has a single reason to change, making the code more modular and easier to maintain.

    OCP-Open/Close principle

    The Open/Closed Principle (OCP) is one of the SOLID principles of object-oriented design, stating that software entities (such as classes, modules, and functions) should be open for extension but closed for modification. This means that the behavior of a system should be extendable without altering its existing code, allowing for new features or functionality to be added with minimal risk of introducing bugs or breaking existing functionality. To achieve this, developers often rely on abstractions (e.g., interfaces or abstract classes) and mechanisms like inheritance or polymorphism, enabling them to add new implementations or behaviors without changing the core logic of the system. By adhering to OCP, systems become more flexible, maintainable, and scalable over time.

    protocol Shape {
        func area() -> Double
    }
    
    struct Circle: Shape {
        let radius: Double
        func area() -> Double {
            return 3.14 * radius * radius
        }
    }
    
    struct Square: Shape {
        let side: Double
        func area() -> Double {
            return side * side
        }
    }
    
    // Adding a new shape without modifying existing code
    struct Triangle: Shape {
        let base: Double
        let height: Double
        func area() -> Double {
            return 0.5 * base * height
        }
    }
    
    // Usage
    let shapes: [Shape] = [
        Circle(radius: 5),
        Square(side: 4),
        Triangle(base: 6, height: 3)
    ]
    
    for shape in shapes {
        print("Area: \(shape.area())")
    }

    In this example the Shape protocol defines a contract for all shapes. New shapes like CircleSquare, and Triangle can be added by conforming to the protocol without modifying existing code. This adheres to OCP by ensuring the system is open for extension (new shapes) but closed for modification (existing code remains unchanged).

    LSP-Liksov substitution principle

    The Liskov Substitution Principle (LSP) is one of the SOLID principles of object-oriented design, named after Barbara Liskov. It states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a subclass should adhere to the contract established by its superclass, ensuring that it can be used interchangeably without causing unexpected behavior or violating the assumptions of the superclass. This principle emphasizes the importance of designing inheritance hierarchies carefully, ensuring that derived classes extend the base class’s functionality without altering its core behavior. Violations of LSP can lead to fragile code, bugs, and difficulties in maintaining and extending the system.

    protocol Vehicle {
        func move()
    }
    
    class Car: Vehicle {
        func move() {
            print("Car is moving")
        }
        
        func honk() {
            print("Honk honk!")
        }
    }
    
    class Bicycle: Vehicle {
        func move() {
            print("Bicycle is moving")
        }
        
        func ringBell() {
            print("Ring ring!")
        }
    }
    
    func startJourney(vehicle: Vehicle) {
        vehicle.move()
    }
    
    let car = Car()
    let bicycle = Bicycle()
    
    startJourney(vehicle: car)      // Output: Car is moving
    startJourney(vehicle: bicycle)  // Output: Bicycle is moving

    In this example, both Car and Bicycle conform to the Vehicle protocol, allowing them to be used interchangeably in the startJourney function without affecting the program’s behavior

    ISP-Interface segregation principle

    The Interface Segregation Principle (ISP) is one of the SOLID principles of object-oriented design, which states that no client should be forced to depend on methods it does not use. In other words, interfaces should be small, specific, and tailored to the needs of the classes that implement them, rather than being large and monolithic. By breaking down large interfaces into smaller, more focused ones, ISP ensures that classes only need to be aware of and implement the methods relevant to their functionality. This reduces unnecessary dependencies, minimizes the impact of changes, and promotes more modular and maintainable code. For example, instead of having a single interface with methods for printing, scanning, and faxing, ISP would suggest separate interfaces for each responsibility, allowing a printer class to implement only the printing-related methods.

    // Violating ISP
    protocol Worker {
        func work()
        func eat()
    }
    
    class Human: Worker {
        func work() {
            print("Human is working")
        }
        func eat() {
            print("Human is eating")
        }
    }
    
    class Robot: Worker {
        func work() {
            print("Robot is working")
        }
        func eat() {
            // Robots don't eat, but forced to implement this method
            fatalError("Robots don't eat!")
        }
    }
    
    // Following ISP
    protocol Workable {
        func work()
    }
    
    protocol Eatable {
        func eat()
    }
    
    class Human: Workable, Eatable {
        func work() {
            print("Human is working")
        }
        func eat() {
            print("Human is eating")
        }
    }
    
    class Robot: Workable {
        func work() {
            print("Robot is working")
        }
    }

    In this example, we first violate ISP by having a single Worker protocol that forces Robot to implement an unnecessary eat() method. Then, we follow ISP by splitting the protocol into Workable and Eatable, allowing Robot to only implement the relevant work() method.

    DIP-Dependency injection principle

    The Dependency Inversion Principle (DIP) is one of the SOLID principles of object-oriented design, which states that high-level modules (e.g., business logic) should not depend on low-level modules (e.g., database access or external services), but both should depend on abstractions (e.g., interfaces or abstract classes). Additionally, abstractions should not depend on details; rather, details should depend on abstractions. This principle promotes loose coupling by ensuring that systems rely on well-defined contracts (interfaces) rather than concrete implementations, making the code more modular, flexible, and easier to test or modify. For example, instead of a high-level class directly instantiating a low-level database class, it would depend on an interface, allowing the database implementation to be swapped or mocked without affecting the high-level logic.

    protocol Engine {
        func start()
    }
    
    class ElectricEngine: Engine {
        func start() {
            print("Electric engine starting")
        }
    }
    
    class Car {
        private let engine: Engine
        
        init(engine: Engine) {
            self.engine = engine
        }
        
        func startCar() {
            engine.start()
        }
    }
    
    let electricEngine = ElectricEngine()
    let car = Car(engine: electricEngine)
    car.startCar() // Output: Electric engine starting

    In this example, the Car class depends on the Engine protocol (abstraction) rather than a concrete implementation, adhering to the DIP

    Conclusions

    We have demonstrated how the SOLID principles align with Swift, so there is no excuse for not applying them. You can find source code used for writing this post in following repository

  • Copy-on-Write in Swift: Managing Value Types

    Copy-on-Write in Swift: Managing Value Types

    Copy on Write (CoW) is an interesting concept because it helps developers understand how Swift optimizes memory management by avoiding unnecessary data duplication. CoW ensures that objects are only copied when modified, saving resources and improving performance. This concept is particularly relevant when working with value types like structs or arrays, where multiple references can exist to the same data.

    By providing an example—such as modifying a struct inside an array—we aim to demonstrate how CoW prevents redundant copying, enhancing both efficiency and memory usage in iOS apps.

    CoW – Copy-on-Write

    Copy on Write (CoW) is an optimization technique used in Swift for value types, particularly for collections like Arrays, Strings, and Dictionaries. It allows these value types to share the same underlying storage until a mutation occurs, improving performance and memory efficiency.

    How Copy on Write Works

    1. Initial Assignment: When a value type is assigned to a new variable, Swift performs a shallow copy, creating a new reference to the same underlying data3.

    2. Shared Storage: Multiple instances of the value type share the same memory location, reducing unnecessary copying.

    3. Mutation Trigger: When one instance attempts to modify its contents, Swift creates a full copy of the data for that instance.

    4. Preserved Originals: The original instance remains unchanged, maintaining value semantics.

    Benefits

    • Performance Optimization: Avoids unnecessary copying of large data structures.

    • Memory Efficiency: Reduces memory usage by sharing data until modification is required.

    • Value Semantics: Maintains the expected behavior of value types while improving efficiency.

    Implementation

    CoW is built into Swift’s standard library for Arrays, Strings, and Dictionaries. For custom value types, developers can implement CoW behavior using techniques like:

    1. Using isUniquelyReferenced to check if a copy is needed before mutation.

    2. Wrapping the value in a reference type (e.g., a class) and managing copies manually.

    It’s important to note that CoW is not automatically available for custom value types and requires explicit implementation.

    Example code this time is provided via Playground:

    import UIKit
    
    final class Wrapper {
        var data: [Int]
        
        init(data: [Int]) {
            self.data = data
        }
    }
    
    struct CoWExample {
        private var storage: Wrapper
    
        init(data: [Int]) {
            self.storage = Wrapper(data: data)
        }
    
        mutating func modifyData() {
            print("Memory @ before updating: \(Unmanaged.passUnretained(storage).toOpaque())")
            
            if !isKnownUniquelyReferenced(&storage) {
                print("Making a copy of the data before modifying it.")
                storage = Wrapper(data: storage.data) // Created a copy
            } else {
                print("Update without copy, unique reference.")
            }
    
            storage.data.append(4)  // Modify array from class inside
            print("@ Memory after updaing: \(Unmanaged.passUnretained(storage).toOpaque())")
        }
    
        func printData(_ prefix: String) {
            print("\(prefix) Data: \(storage.data) | Memory @: \(Unmanaged.passUnretained(storage).toOpaque())")
        }
    }
    
    // Use  Copy-on-Write
    var obj1 = CoWExample(data: [1, 2, 3])
    var obj2 = obj1  // Both instances share same memory @
    
    print("Before updating obj2:")
    obj1.printData("obj1:")
    obj2.printData("obj2:")
    
    print("\nUpdating obj2:")
    obj2.modifyData() // Here will take place copy when there's a new reference
    
    print("\nAfter updating obj1:")
    obj1.printData("obj1:")
    obj2.printData("obj2:")

    Key Components:

    1. Wrapper Class:

      • final class that holds an array of integers (data).

      • It is used as the underlying storage for the CoWExample struct.

    2. CoWExample Struct:

      • Contains a private property storage of type Wrapper.

      • Implements the Copy-on-Write mechanism in the modifyData() method.

      • Provides a method printData(_:) to print the current data and memory address of the storage object.

    3. modifyData() Method:

      • Checks if the storage object is uniquely referenced using isKnownUniquelyReferenced(&storage).

      • If the storage object is not uniquely referenced (i.e., it is shared), a new copy of the Wrapper object is created before modifying the data.

      • If the storage object is uniquely referenced, the data is modified directly without creating a copy.

      • Appends the value 4 to the data array.

    4. Memory Address Tracking:

      • The memory address of the storage object is printed before and after modification to demonstrate whether a copy was made.

    Run playground

    Lets run the playground and analyze logs:

    After obj2=obj1 assignation, both share exactly same memory @ddress. But when obj2 is being updated:

    Is at that moment when obj2 is allocated in a different @dress space and then updated.

    Conclusions

    A quick answer is that the difference between Value Types and Reference Types lies in how they are assigned. When a Value Type is assigned, an isolated copy is created. However, in reality, after assignment, Value Types behave similarly to Reference Types. The key difference emerges when an update occurs—at that point, a new allocation is performed.

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

    References

  • Breaking Retain Cycles in Swift

    Breaking Retain Cycles in Swift

    Detecting and preventing retain cycles is crucial, as they lead to memory leaks, degrade app performance, and cause unexpected behaviors. Many developers, especially those new to Swift and UIKit, struggle with understanding strong reference cycles in closures, delegates, and class relationships.

    We will present two classic retain cycle bugs in a sample iOS app, explore the tools that Xcode provides for detecting them, and share some advice on how to avoid them.

    Memory Graph Debuger

    The sample application consists of two view screens. The pushed screen contains injected retain cycles, leading to memory leaks. A memory leak occurs when memory references cannot be deallocated. In this app, the leak happens when the pushed screen is popped back but remains in memory.

    Build and deploy app on simulator (or real device): 

    Open Memory Graph Debuger

    In this case is clear where do we have a retain cycle.

    class MyViewModel: ObservableObject {
        @Published var count: Int = 0
        var classA: ClassA  = ClassA()
        
        var incrementClosure: (() -> Void)?
        
        init() {
    ...
            
            #if true
            incrementClosure = {
                self.count += 1
            }
            #else
    ...
            }
            #endif
        }
        
        deinit {
            print("MyViewModel is being deallocated")
        }
    }
    
    struct SecondView: View {
        @StateObject private var viewModel = MyViewModel()
        var body: some View {

    In SecondView, MyViewModel is referenced using viewModel, MyViewModel.incrementalClosure, and self, which also references MyViewModel indirectly. When the view is popped, this class cannot be removed from memory because it is retained due to an internal reference from self.count.

    If you set a breakpoint in the deinit method, you will notice that it is never triggered. This indicates that the class is still retained, leading to a memory leak. As a result, the memory allocated for MyViewModel will never be deallocated or reused, reducing the available memory for the app. When the app runs out of memory, iOS will forcefully terminate it.

    The only way to break this retain cycle is to make one of these references weak. Using a weak reference ensures that it is not counted toward the retain count. When the view is popped, SecondView holds the only strong reference, allowing iOS to deallocate MyViewModel and free up memory.

    This is the correct solution:

    class MyViewModel: ObservableObject {
        @Published var count: Int = 0
        var classA: ClassA  = ClassA()
        
        var incrementClosure: (() -> Void)?
        
        init() {
            ...
            
            #if false
          ....
            #else
            incrementClosure = { [weak self] in
                self?.count += 1
            }
            #endif
        }
        
        deinit {
            print("MyViewModel is being deallocated")
        }
    }

    Set a breakpoint in deinit to verify that the debugger stops when the view is popped. This confirms that the class has been properly deallocated

    Next retain cycle is a memory reference cycle, when we have a chain of refenced classes and once of them is referencing back it generates a loop of references. For implementing this memory leak we have created a classA that references a classB that references a classC that finally refences back to classA.

    Here we can see clear that same memory address is referenced. But if we take a look at Debug Memory Inspector

    It is not as clear as the previous case. This is a prepared sample app, but in a real-world application, the graph could become messy and make detecting memory leaks very difficult. Worst of all, with this kind of memory leak, when the view is removed, the deinit method is still being executed.

    For detecting such situations we will have to deal with another tool.

    Insruments

    Xcode Instruments is a powerful performance analysis and debugging tool provided by Apple for developers to profile and optimize their iOS, macOS, watchOS, and tvOS applications. It offers a suite of tools that allow developers to track memory usage, CPU performance, disk activity, network usage, and other system metrics in real-time. Instruments work by collecting data through time-based or event-based profiling, helping identify performance bottlenecks, memory leaks, and excessive resource consumption. Integrated within Xcode, it provides visual timelines, graphs, and detailed reports, making it an essential tool for fine-tuning app efficiency and responsiveness.

    In XCode Product menu select Profile:

    For measuring memory leaks select ‘Leaks»:

    Press record button for deploying on simulator and start recording traces.

    In following video, you will see that when view is pushed back then memory leak is detected:

    Is programed  to check memory every 10 seconds, when we click on red cross mark then bottom area shows the classes affected:

    Conclusions

    In this post, I have demonstrated how to detect memory leaks using the Memory Graph Debugger and Inspector. However, in my opinion, preventing memory leaks through good coding practices is even more important than detecting them.

    In Swift, memory leaks typically occur due to retain cycles, especially when using closures and strong references. To avoid memory leaks, you can use weak references where appropriate.

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

    References

  • iOS NFC Development: From URLs to Deeplinks

    iOS NFC Development: From URLs to Deeplinks

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

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

    Storing web url into NFC tag

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

    Deeplinks

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

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

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

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

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

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

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

    It would be called in the following way for creating deeplink

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

    Deploy project on a real device for validating behaviour

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

    Conclusions

    In this post, I have extended the functionalities we can implement with NFC tags by using URLs. You can find source code used for writing this post in following repository.

    References

  • Harnessing NFC Technology in your iOS App

    Harnessing NFC Technology in your iOS App

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

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

    Requirements

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

    Base project and NFC configuration

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

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

    Finally setup entitlements for allowing working with NDEF tags:

    NFC sample application

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

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

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

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

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

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

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

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

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

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

    Conclusions

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

    References

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

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

  • DebugSwift: Streamline Your Debugging Workflow

    DebugSwift: Streamline Your Debugging Workflow

    Developing an iOS app using DebugSwift is highly beneficial, as it provides powerful debugging tools specifically designed for Swift developers. This tool simplifies the debugging process by offering an intuitive interface to inspect variables, view complex data structures, and debug Swift code more efficiently. By making runtime debugging more accessible and improving code visibility during execution, DebugSwift helps reduce development time and is especially valuable for resolving issues in complex Swift applications.

    In this post, we will demonstrate how to configure the tool, track API REST service calls, and explore some additional utilities.

    Base project

     

    Here’s a revised version of your text for improved clarity, grammar, and flow:


    The base code for this project is a straightforward iOS list-detail application. It makes a request to the Rick and Morty API to retrieve character information and fetches their corresponding images. It’s as simple as that:

    For installing DebugSwift SPM package just go to project settings, package dependencies: 

    We need a sample user gesture to trigger the tool, the most common event is shake, so we will creat a new modifier for controlling this event on any view:

    import SwiftUI
    #if DEBUG
        import DebugSwift
    #endif
    
    @main
    struct DebugSwiftAppDemoApp: App {
    
        var body: some Scene {
            WindowGroup {
                CharacterView()
                    .onAppear {
                    #if DEBUG
                        setupDebugSwift()
                    #endif
                }
                .onShake {
                    #if DEBUG
                        DebugSwift.show()
                    #endif
                }
            }
        }
    
        fileprivate func setupDebugSwift() {
            DebugSwift
                .setup()
            // MARK: - Enable/Disable Debugger
            DebugSwift.Debugger.logEnable = true
            DebugSwift.Debugger.feedbackEnable = true
        }
    }

    This SwiftUI app integrates the DebugSwift framework for debugging purposes, enabled only in DEBUG mode. It displays a CharacterView in its main scene and includes features for debugging during development. When the view appears, it initializes the DebugSwift setup, enabling logging and user feedback. Additionally, a shake gesture triggers the display of the DebugSwift debugging interface, offering developers a quick way to access debugging tools.

    The use of conditional compilation (#if DEBUG) ensures that all DebugSwift functionality is included only in development builds and excluded from production (RELEASE mode). This approach allows for powerful debugging capabilities during development while maintaining a clean and secure production build.

    .onShake is a custom modifier that executes an action when a shake event is detected. The focus of this post is not to explain its implementation, but you can find a link to the repository at the end of the post.

     

    Let’s debug…

    All setup is ready, if you deployed:

    • On a real device, just share once the app is presente.
    • On simulator, Simulator, Device, Shake:

     

    It will appear an small button at the center left of the screen:

    The number «21» displayed in the middle of the button represents the total number of API requests made so far. You can perform either a short press or a long press on the button. A long press opens the Debug View Hierarchy, which will be discussed in the upcoming sections, specifically in the context of using a device or simulator. For now, just perform a short press.

    Network

    The first screen presented is the Network view, which I personally use the most. It allows you to review API requests and responses, making it easier to determine whether a communication issue originates upstream (backend), downstream (frontend), or even both!

    This is the ordered list of requests made by the app. The first request retrieves the characters, while the subsequent ones primarily fetch the .jpeg images for these characters. If we tap on the first element:

    We can see detailed API headers, request, response, response times, and more. On the navigation bar, from left to right, there are three interesting options:

    1. Share this information
    2. Get the cURL command to execute the same request in the command line:
      bash:
      curl -X GET -H "" -d "" https://rickandmortyapi.com/api/character
    3. Copy this information to paste elsewhere.

    Performance

    It allows you to monitor CPU usage, memory usage, frames per second, and memory leaks in real-time.

    User interface

    There are many utilities related to the user interface, such as:

    • Colorized view borders (as shown in the picture below)
    • Slow animations
    • Show touches
    • Switch to dark mode

    There is also a grid overlay utility that displays a grid, which can be quite useful for adjusting margins. In the screenshot below, I have set the grid to 20×20:

    App resources

    Is possible also review app resources, such as:

    • App folders (Document,  Library, SystemData, tmp) and its files
    • App user defalts
    • App secured data stored in Keychain

    Extend debug tool

    You heard well you can extend, the tool for presentin specific information from current App,  impelemnting actions that are specific on your app. In configuration section is where  the magic takes place:

    fileprivate func setupDebugSwift() {
            DebugSwift
                .setup()
            // MARK: - Custom Info
    
            DebugSwift.App.customInfo = {
                [
                        .init(
                        title: "Info 1",
                        infos: [
                                .init(title: "title 1", subtitle: "subtitle 1")
                        ]
                    )
                ]
            }
    
            // MARK: - Custom Actions
    
            DebugSwift.App.customAction = {
                [
                        .init(
                        title: "Action 1",
                        actions: [
                                .init(title: "action 1") { // [weak self] in
                                print("Action 1")
                            }
                        ]
                    )
                ]
            }
    
            // MARK: Leak Detector
    
            DebugSwift.Performance.LeakDetector.onDetect { data in
                // If you send data to some analytics
                print(data.message)
            }
    
            // MARK: - Custom Controllers
    
             DebugSwift.App.customControllers = {
                 let controller1 = UITableViewController()
                 controller1.title = "Custom TableVC 1"
    
                 let controller2 = UITableViewController()
                 controller2.title = "Custom TableVC 2"
    
                 return [controller1, controller2]
             }
    
            // MARK: - Enable/Disable Debugger
            DebugSwift.Debugger.logEnable = true
            DebugSwift.Debugger.feedbackEnable = true
        }

    This is presented in the following way:

    If your custom debug data is too complex, you can dedicate an entire view to it:

    Just one more thing…

    I mentioned at the beginning that you can also perform a long press after shaking to trigger the DebugSwift tool interface. Please try it now. You should see the View Hierarchy:

    But also Debug View Hierarchy:

    Conclusions

    I hope that you have enjoyed same as me discovering succh useful tool. You can find the source code used in this post in the following repository.

    References

  • Swift 6 migration recipes

    Swift 6 migration recipes

    Swift’s concurrency system, introduced in Swift 5.5, simplifies the writing and understanding of asynchronous and parallel code. In Swift 6, language updates further enhance this system by enabling the compiler to ensure that concurrent programs are free of data races. With this update, compiler safety checks, which were previously optional, are now mandatory, providing safer concurrent code by default.

    Sooner or later, this will be something every iOS developer will need to adopt in their projects. The migration process can be carried out incrementally and iteratively. The aim of this post is to present concrete solutions for addressing specific issues encountered while migrating code to Swift 6. Keep in mind that there are no silver bullets in programming.

    Step 0. Plan your strategy

    First, plan your strategy, and be prepared to roll back and re-plan as needed. That was my process, after two rollbacks 🤷:

    1. Set the «Strict Concurrency Checking» compiler flag on your target. This will bring up a myriad of warnings, giving you the chance to tidy up your project by removing or resolving as many warnings as possible before proceeding.
    2. Study Migration to Swift 6 (from swift.org), Don’t just skim through it; study it thoroughly. I had to roll back after missing details here. 
    3. Set the «Strict Concurrency Checking» compiler flag to Complete. This will trigger another round of warnings. Initially, focus on moving all necessary elements to @MainActor to reduce warnings. We’ll work on reducing the main-thread load later.
      1. Expect @MainActor propagation. As you apply @MainActor, it’s likely to propagate. Ensure you also mark the callee functions as @MainActor where needed
      2. In protocol delegate implementations, verify that code runs safely on @MainActor. In some cases, you may need to make a copy of parameters to prevent data races.
      3. Repeat the process until you’ve resolved all concurrency warnings.
      4. Check your unit tests, as they’ll likely be affected. If all is clear, change targets and repeat the process..
    4. Set the Swift Language Version to Swift 6 and run the app on a real device to ensure it doesn’t crash. I encountered a crash at this stage..
    5. Reduce @MainActor usage where feasible. Analyze your code to identify parts that could run in isolated domains instead. Singletons and API services are good candidates for offloading from the main thread.

     

    On following sections I will explain the issues that I had found and how I fix them.

    Issues with static content

    Static means that the property is shared across all instances of that type, allowing it to be accessed concurrently from different isolated domains. However, this can lead to the following issue:

    Static property ‘shared’ is not concurrency-safe because non-‘Sendable’ type ‘AppGroupStore’ may have shared mutable state; this is an error in the Swift 6 language mode

    The First-Fast-Fix approach here is to move all classes to @MainActor.

    @MainActor
    final class AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.EMOM-timers")
        static let shared = AppGroupStore()
        private init() {
        }
    }

    Considerations:

    Moving to @MainActor forces all calls to also belong to @MainActor, leading to a propagation effect throughout the codebase. Overusing @MainActor may be unnecessary, so in the future, we might consider transitioning to an actor instead.

    For now, we will proceed with this solution. Once the remaining warnings are resolved, we will revisit and optimize this approach if needed.

     

    Issues with protocol implementation in the system or third-party SDK.

    First step: focus on understanding not the origin, but how this data is being transported. How is the external dependency handling concurrency? On which thread is the data being dellivered? As a first step, check the library documentation — if you’re lucky, it will have this information. For instance:

    • CoreLocation: Review the delegate specification in the CLLocationManager documentation. At the end of the overview, it specifies that callbacks occur on the same thread where you initialized the CLLocationManager.
    • HealthKit: Consult the HKWorkoutSessionDelegate documentation. Here, the overview mentions that HealthKit calls these methods on an anonymous serial background queue.
     

    In my case, I was working with WatchKit and implementing the WCSessionDelegate. The documentation states that methods in this protocol are called on a background thread.

    Once I understand how the producer delivers data, I need to determine the isolated domain where this data will be consumed. In my case, it was the @MainActor due to recursive propagation from @MainActor.

    Now, reviewing the code, we encounter the following warning:

    Main actor-isolated instance method ‘sessionDidBecomeInactive’ cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode.

    In case this method had no implementation just mark it as nonisolated and move to next:

    nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
     }

    The next delegate method does have an implementation and runs on the @MainActor, while the data is delivered on a background queue, which is a different isolated domain. I was not entirely sure whether the SDK would modify this data.

    My adaptation at that point was create a deep copy of data received, and forward the copy to be consumed..

    nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
         doCopyAndCallUpdateInMainActor(userInfo)
    }
       
    private nonisolated func doCopyAndCallUpdateInMainActor(_ dictionary: [String: Any] = [:])  {
        nonisolated(unsafe) let dictionaryCopy =  dictionary.deepCopy()
            Task { @MainActor in
                await self.update(from: dictionaryCopy)
            }
    }

    Issues with default parameter function values

    I have a function that receives a singleton as a default parameter. The singletons are working in an isolated domain; in this case, it was under @MainActor. I encountered the following issue:

    Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode

    To remove this warning, I made it an optional parameter and handled its initialization:

    init(audioManager: AudioManagerProtocol? = nil,
             extendedRuntimeSessionDelegate: WKExtendedRuntimeSessionDelegate? = nil) {
            self.audioManager = audioManager ?? AudioManager.shared
            self.extendedRuntimeSessionDelegate = extendedRuntimeSessionDelegate
        }

    Decoupling non-UI logic from @MainActor for better performance.

    There are components in your application, such as singletons or APIs, that are isolated or represent the final step in an execution flow managed by the app. These components are prime candidates for being converted into actors.

    Actors provide developers with a means to define an isolation domain and offer methods that operate within this domain. All stored properties of an actor are isolated to the enclosing actor instance, ensuring thread safety and proper synchronization.

    Previously, to expedite development, we often annotated everything with @MainActor.

    @MainActor
    final class AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.XYZ")
        
        static let shared = AppGroupStore()
        
        private init() {
            
        }
    }

    Alright, let’s move on to an actor.

    actor AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.XYZ")
        
        static let shared = AppGroupStore()
        
        private init() {
            
        }
    }

    Compiler complains:

    Actor ‘AppGroupStore’ cannot conform to global actor isolated protocol ‘AppGroupStoreProtocol’

    That was because protocol definition was also @MainActor, so lets remove it:

    import Foundation
    //@MainActor
    protocol AppGroupStoreProtocol {
        func getDate(forKey: AppGroupStoreKey) -> Date?
        func setDate(date: Date, forKey: AppGroupStoreKey)
    }

    At this point, the compiler raises errors for two functions: one does not return anything, while the other does. Therefore, the approaches to fixing them will differ.

    We have to refactor them to async/await

    protocol AppGroupStoreProtocol {
        func getDate(forKey: AppGroupStoreKey) async -> Date?
        func setDate(date: Date, forKey: AppGroupStoreKey) async
    }

    There are now issues in the locations where these methods are called.

    This function call does not return any values, so enclosing its execution within Task { ... } is sufficient.

        func setBirthDate(date: Date) {
            Task {
                await AppGroupStore.shared.setDate(date: date, forKey: .birthDate)
            }
        }

    Next isssue is calling a function that in this case is returning a value, so the solution will be different.

    Next, the issue is calling a function that, in this case, returns a value, so the solution will be different.

        func getBirthDate() async -> Date {
            guard let date = await AppGroupStore.shared.getDate(forKey: .birthDate) else {
                return Calendar.current.date(byAdding: .year, value: -25, to: Date()) ?? Date.now
            }
            return date
        }

    Changing the function signature means that this change will propagate throughout the code, and we will also have to adjust its callers.

    Just encapsulate the call within a Task{...}, and we are done.

            .onAppear() {
                Task {
                    guard await AppGroupStore.shared.getDate(forKey: .birthDate) == nil else { return }
                    isPresentedSettings.toggle()
                }
            }

    Conclusions

    To avoid a traumatic migration, I recommend that you first sharpen your saw. I mean, watch the WWDC 2024 videos and study the documentation thoroughly—don’t read it diagonally. Once you have a clear understanding of the concepts, start hands-on work on your project. Begin by migrating the easiest issues, and as you feel more comfortable, move on to the more complicated ones.

    At the moment, it’s not mandatory to migrate everything at once, so you can start progressively. Once you finish, review if some components could be isolated into actors.

    Related links