Etiqueta: API Rest

  • QR Code Scanning in iOS

    QR Code Scanning in iOS

    Scanning a URL encoded within a QR code and fetching its contents is particularly interesting because it addresses a common real-world scenario faced by developers. QR codes are widely used for sharing links, event details, and more, making it essential for iOS apps to handle them efficiently.

    This post will not only guide developers through implementing QR code scanning using native frameworks like AVFoundation but also demonstrate how to seamlessly fetch and process the retrieved URL content using URLSession.

    We will implement a client-server application where the server will provide a QR image and implement REST API services encoded within the QR code. The client will be an iOS app that scans the QR code and fetches the REST API service.

    QR-Server

    For the server, we will implement a Dockerized Node.js server to create a blank project.

    npm init -y

    Later on, we will need to integrate ‘express’ and ‘qrcode’ into the project.

    npm install express qrcode

    Server code is following:

    const express = require('express');
    const QRCode = require('qrcode');
    const os = require('os');
    
    const app = express();
    const port = 3000;
    const hostIP = process.env.HOST_IP || 'Desconocida';
    
    app.get('/', (req, res) => {
        const url = `http://${hostIP}:${port}/data`;
        QRCode.toDataURL(url, (err, qrCode) => {
            if (err) res.send('Error generating QR code');
            res.send(`<!DOCTYPE html><html><body>
            <h2>Scan the QR code:</h2>
            <img src="${qrCode}" />
            </body></html>`);
        });
    });
    
    app.get('/data', (req, res) => {
        res.json({ message: 'Hello from QR-API-Server!' });
    });
    
    app.listen(port, () => {
        console.log(`Server running on http://${hostIP}:${port}`);
    });

    This Node.js script sets up an Express.js web server that generates a dynamic QR code. When the root URL («/») is accessed, the server creates a QR code containing a URL pointing to the /data endpoint. The QR code is displayed on an HTML page with the message «Scan the QR code.»

    Accessing the /data endpoint returns a JSON object with the message: «Hello from QR-API-Server!». The server listens on port 3000, and the host IP address is either obtained from an environment variable or defaults to 'Desconocida' (Unknown) if not specified.

    Next step: set up the Dockerfile.

    # Base image for Node.js
    FROM node:14
    
    # Create and fix working dirctory
    WORKDIR /usr/src/app
    
    # Copy application files
    COPY . .
    
    # Install dependencies
    RUN npm install
    
    # Expose appliction port
    EXPOSE 3000
    
    # Command for starting server application
    CMD ["node", "server.js"]

    The Dockerfile sets up a Docker image for a Node.js application. It starts by using the official Node.js 14 base image. It then creates and sets the working directory to /usr/src/app. The application files from the local directory are copied into this working directory. Next, it installs the necessary dependencies using npm install. The image exposes port 3000, indicating that the application will listen on that port. Finally, the Docker container will run the Node.js server application by executing node server.js when started.

    Get back to command line and create docker image:

    docker build -t qr-server .

    And finally run the image:

    docker run -d -p 3000:3000 \ 
    -e HOST_IP=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -n 1) qr-server

    You need to provide the host Docker machine’s container runner IP to allow an iOS app running on a real device (due to camera scanning) to access the server.

    QR Scaner iOS App

    Client is an iOS sample Scan app designed to scan QR codes and call the service endpoint encoded within the QR code. To perform scanning, the app will require access to the camera.

    Open target build settings and fill in ‘Privacy – Camera Usage Description’. View code is following:

    struct ContentView: View {
        @State private var scannedURL: String? = nil
        @State private var dataFromAPI: String? = nil
        @State private var showAlert = false
        @State private var alertMessage = ""
    
        var body: some View {
            VStack {
                if let scannedURL = scannedURL {
                    Text("URL scanned: \(scannedURL)")
                        
                        .padding()
    
                    Button("Do API Call") {
                        Task {
                            await fetchAPIData(from: scannedURL)
                        }
                    }
                    .padding()
    
                    if let dataFromAPI = dataFromAPI {
                        Text("Data from API: \(dataFromAPI)")
                            .padding()
                    }
                } else {
                    ZStack {
                        
                        QRCodeScannerView {
                            self.scannedURL = $0
                        }
                        .edgesIgnoringSafeArea(.all)
                        Text("Scan QR code:")
                    }
    
                }
            }
            .font(.title)
            .alert(isPresented: $showAlert) {
                Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
            }
        }
    
        func fetchAPIData(from url: String) async {
            guard let url = URL(string: url) else { return }
    
            do {
                let (data, response) = try await URLSession.shared.data(from: url)
                if let result = String(data: data, encoding: .utf8) {
                    dataFromAPI = result
                }
            } catch {
                alertMessage = "Error: \(error.localizedDescription)"
                showAlert = true
            }
        }
    }

    SwiftUI code creates a ContentView that first scans a QR code to extract a URL, displays the scanned URL, and provides a button to fetch data from that URL via an API call, showing the result or an error alert if the request fails. The interface initially shows a QR scanner overlay with the prompt «Scan QR code,» and upon successful scanning, it displays the URL and a button to trigger the API call, which asynchronously retrieves and displays the data or shows an error message in an alert if something goes wrong. The layout uses a vertical stack (VStack) to organize the UI elements and adjusts fonts and padding for better readability.

    QRCodeScannerView has to be implemented by using UIKit-UIViewControllerRepresentable bridge compontent:

    import SwiftUI
    import AVFoundation
    
    struct QRCodeScannerView: UIViewControllerRepresentable {
        class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
            var parent: QRCodeScannerView
    
            init(parent: QRCodeScannerView) {
                self.parent = parent
            }
    
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    parent.didFindCode(stringValue)
                }
            }
        }
    
        var didFindCode: (String) -> Void
    
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
    
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                return viewController
            }
    
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            Task {
                captureSession.startRunning()
            }
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
    }

    The code creates a QR code scanner view that uses AVFoundation to capture and process QR codes, where it sets up a camera preview layer, configures a capture session to detect QR codes, and triggers a vibration and callback function (didFindCode) when a QR code is successfully scanned, passing the decoded string value to the parent view. The UIViewControllerRepresentable protocol bridges UIKit’s AVCaptureMetadataOutput functionality into SwiftUI, with a Coordinator class handling the metadata output delegation to process detected codes.

    Important point for scanning you need to deploy on a real iOS device:

    As soon as the QR code URL is read, it is presented to the user. When the user presses ‘Do API Call’, the iOS app performs a request to the service.

    Conclusions

    In this post I have presented how to consume API services where endpoints are could be dynamically provided by server via QR codes. If you’re interested in exploring the implementation further, you can find the source code used for this post in the following repository

  • Crafting a Simple iOS App Using GraphQL APIs

    Crafting a Simple iOS App Using GraphQL APIs

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

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

    Setup a graphQL Server

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

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

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

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

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

    To build the Docker image, use the following command:

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

    Sample iOS graphQL client app

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

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

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

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

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

    Conclusions

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

    References

  • Dealing a REST API with Combine

    Dealing a REST API with Combine

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

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

    Starting Point for Base Code

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

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

    Publisher

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

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

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

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

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

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

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

    Let’s dive into CharacterServiceComb:

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

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

    Finally, CharacterServiceComb:

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

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

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

    Future

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

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

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

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

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

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

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

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

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

    Finally, CharacterServiceComb:

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

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

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

    Publisher vs Future

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

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

    Conclusions

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

    References

  • 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