Categoría: Uncategorized

  • Tired of Repeating Configs in Every Target?

    Tired of Repeating Configs in Every Target?

    Centralizing configuration parameters across multiple iOS targets is a valuable approach, especially in larger or modularized projects. Maintaining separate settings for each target often leads to duplication, inconsistency, and errors. Developers frequently struggle to keep build settings, API endpoints, feature flags, and environment variables in sync across targets such as staging, production, or app extensions.

    By demonstrating how to structure and manage these settings in a clean, scalable way—using tools like xcconfig files or centralized Swift structs—you can enhance maintainability, reduce bugs, and promote best practices in professional iOS development.

    In this post, we’ll walk through an example of centralizing the project and build version across multiple targets.

    Note: The goal here is not to convince you to centralize all configurations, but to show you how to do it effectively if your project requires it.

    Multiple version values

    One common problem when having more than one target app is managing multiple version values across different targets for the same app version.

    In this case, MARKETING_VERSION and CURRENT_PROJECT_VERSION are defined in three places: the project build settings and each target’s build settings. We want to define them at the project level, and have each target inherit these values from the project.

    To do this, select the CenterXCodeConfigs target:

    And replace 1 by $(CURRENT_PROJECT_VERSION), and also

    1.0 by $(MARKETING_VERSION). Switch to project build settings:

    Now, fill in the current project version and marketing version with the desired values, then switch back to the CenterXCodeConfigs target’s build settings.

    Voilà! Values are now inherited from the project settings. Repeat the same operation for the AlternativeApp target.

    Conclusions

    In this post, I presented how to centralize common settings values across all targets. You can find source code used for writing this post in following repository

  • 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

  • Dynamic Forms in SwiftUI for variable section type

    Dynamic Forms in SwiftUI for variable section type

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

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

    Dynamic sample SwiftUI app

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

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

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

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

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

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

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

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

    Conclusions

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

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

    References

  • 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

  • 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

  • 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