Etiqueta: Swift

  • 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