Etiqueta: SwiftUI

  • Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Seamless Apple Sign-In for iOS Apps with a Node.js Backend

    Implementing Sign in with Apple in a client-server setup is valuable because it addresses a real-world need that many developers face, especially as Apple requires apps offering third-party login to support it. While Apple’s documentation focuses mainly on the iOS side, there’s often a gap in clear explanations for securely validating Apple ID tokens on the backend — a critical step to prevent security vulnerabilities.

    Since Node.js is a widely used backend for mobile apps, providing a practical, end-to-end guide would help a large audience, fill a common knowledge gap, and position you as an expert who understands both mobile and server-side development, making the post highly useful, shareable, and relevant.

    Apple Sign in

    Apple Sign In is a secure authentication service introduced by Apple in 2019, allowing users to log in to apps and websites using their Apple ID. It emphasizes privacy by minimizing data sharing with third parties, offering features like hiding the user’s real email address through a unique, auto-generated proxy email. Available on iOS, macOS, and web platforms, it provides a fast and convenient alternative to traditional social logins like Google or Facebook.

    Advantages of Apple Sign In
    One of the biggest advantages is enhanced privacy—Apple does not track user activity across apps, and the «Hide My Email» feature protects users from spam and data leaks. It also simplifies the login process with Face ID, Touch ID, or device passcodes, reducing password fatigue. Additionally, Apple Sign In is mandatory for apps that offer third-party logins on iOS, ensuring wider adoption and consistent security standards.

    Inconveniences of Apple Sign In
    A major drawback is its limited availability, as it only works on Apple devices, excluding Android and Windows users. Some developers also criticize Apple for forcing its use on iOS apps while restricting competitor login options. Additionally, if a user loses access to their Apple ID, account recovery can be difficult, potentially locking them out of linked services. Despite these issues, Apple Sign In remains a strong choice for privacy-focused users.

    Dockerized Node.JS server side

    Start by setting up a blank Node.js server using Express.js to handle HTTP requests.

    npm init -y

    Server.js code is following:

    const express = require('express');
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware for parsing JSON
    app.use(express.json());
    
    // Client for look up public keys at Apple
    const client = jwksClient({
        jwksUri: 'https://appleid.apple.com/auth/keys'
    });
    
    // Function for getting public key
    function getAppleKey(header, callback) {
        client.getSigningKey(header.kid, function (err, key) {
            if (err) {
                callback(err);
            } else {
                const signingKey = key.getPublicKey();
                callback(null, signingKey);
            }
        });
    }
    
    // Route for authenticate
    app.post('/auth/apple', (req, res) => {
        const { identityToken } = req.body;
    
        if (!identityToken) {
            return res.status(400).json({ error: 'identityToken missing' });
        }
    
        jwt.verify(identityToken, getAppleKey, {
            algorithms: ['RS256']
        }, (err, decoded) => {
            if (err) {
                console.error('Error verifying token:', err);
                return res.status(401).json({ error: 'Invalid token' });
            }
    
            // decoded contains user data
            console.log('Token verified:', decoded);
    
            res.json({
                success: true,
                user: {
                    id: decoded.sub,
                    email: decoded.email,
                    email_verified: decoded.email_verified
                }
            });
        });
    });
    
    app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
    });
    

    server.js sets up an Express server that listens for authentication requests using Apple’s Sign-In service. It imports necessary modules like express for routing, jsonwebtoken for verifying JSON Web Tokens (JWTs), and jwks-rsa for retrieving Apple’s public keys used to validate tokens. The server is configured to parse incoming JSON payloads and uses environment variables (loaded via dotenv) to optionally define a custom port.

    The core logic resides in the /auth/apple POST route. When a client sends a request to this endpoint with an identityToken in the body (typically issued by Apple after a successful login), the server first checks if the token is present. It then verifies the token using jsonwebtoken.verify(), passing a custom key retrieval function (getAppleKey). This function uses the jwksClient to fetch the appropriate public key from Apple’s JWKS (JSON Web Key Set) endpoint based on the kid (Key ID) found in the token header.

    If the token is valid, the decoded payload—which includes user-specific data like sub (user ID), email, and email_verified—is extracted and returned in the response as JSON. If token verification fails, an error response with HTTP 401 status is sent. This setup allows backend applications to securely validate Apple identity tokens without hardcoding public keys, keeping the authentication mechanism both dynamic and secure.

    Server is dockerized:

    FROM node:20
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    This Dockerfile sets up a Node.js environment using the node:20 base image, creates a working directory at /usr/src/app, copies package.json and package-lock.json (if present) into it, installs dependencies with npm install, copies the rest of the application files, exposes port 3000 for the container, and finally runs the npm start command to launch the application.

    For building the app just type:

    docker build -t apple-signin-server .

    Finally execute the container:

    docker run -p 3000:3000 apple-signin-server

    Server ready for receiving requests…

    Client iOS Apple Sign in app

    After creating a simple iOS app project, go to the target settings and add the ‘Sign in with Apple’ capability. Then, start by creating a blank Node.js server.

    The next step is the client code itself:

    import SwiftUI
    import AuthenticationServices
    
    struct ContentView: View {
        @State private var userID: String?
        @State private var userEmail: String?
        @State private var userName: String?
        
        var body: some View {
            VStack(spacing: 20) {
                if let userID = userID {
                    Text("Welcome 🎉")
                        .font(.title)
                    Text("User ID: \(userID)")
                    if let name = userName {
                        Text("Name: \(name)")
                    }
                    if let email = userEmail {
                        Text("Email: \(email)")
                    }
                } else {
                    SignInWithAppleButton(
                        .signIn,
                        onRequest: { request in
                            request.requestedScopes = [.fullName, .email]
                        },
                        onCompletion: { result in
                            switch result {
                            case .success(let authorization):
                                handleAuthorization(authorization)
                            case .failure(let error):
                                print("Authentication error: \(error.localizedDescription)")
                            }
                        }
                    )
                    .signInWithAppleButtonStyle(.black)
                    .frame(width: 280, height: 50)
                    .cornerRadius(8)
                    .padding()
                }
            }
            .padding()
        }
        
        private func handleAuthorization(_ authorization: ASAuthorization) {
            if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
                userID = appleIDCredential.user
                userEmail = appleIDCredential.email
                if let fullName = appleIDCredential.fullName {
                    userName = [fullName.givenName, fullName.familyName]
                        .compactMap { $0 }
                        .joined(separator: " ")
                }
                
                if let identityToken = appleIDCredential.identityToken,
                   let tokenString = String(data: identityToken, encoding: .utf8) {
                    authenticateWithServer(identityToken: tokenString)
                }
            }
        }
        
        private func authenticateWithServer(identityToken: String) {
            guard let url = URL(string: "http://localhost:3000/auth/apple") else { return }
            
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            let body = ["identityToken": identityToken]
            
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let data = data,
                   let json = try? JSONSerialization.jsonObject(with: data) {
                    print("Server response:", json)
                } else {
                    print("Error communicating with server:", error?.localizedDescription ?? "Unknown error")
                }
            }.resume()
        }
    }
    
    
    #Preview {
        ContentView()
    }
    

    It defines a user interface for an iOS app that integrates Sign in with Apple. The core logic is built into the ContentView struct, which maintains state variables to store the signed-in user’s ID, name, and email. When the view is rendered, it checks whether the user is already signed in (i.e., if userID is not nil). If the user is authenticated, it displays a welcome message along with the retrieved user details. If not, it shows a «Sign in with Apple» button that initiates the authentication process when tapped.

    When the «Sign in with Apple» button is pressed, it triggers a request for the user’s full name and email. The result of this action is handled in the onCompletion closure. If the sign-in is successful, the handleAuthorization method is called. This function extracts the user’s credentials from the ASAuthorizationAppleIDCredential object, including their user ID, email, and full name (if provided). It also extracts the identity token (a JSON Web Token), which is used to authenticate the user on the app’s backend server.

    The authenticateWithServer function handles the server-side communication. It sends a POST request to http://localhost:3000/auth/apple, passing the identityToken in the JSON body. This token can be verified on the backend to ensure the identity is legitimate and secure. The response from the server (or any error encountered) is printed to the console. This architecture supports secure, privacy-preserving user login using Apple’s authentication services, commonly used in modern iOS apps.

    Apple Sign in integration

    Deploy iOS app with Apple Sign-In in a simulator (not on a real device).

    review

    Simply sign in using your personal iCloud credentials. Once Apple Sign-In is successful on the client side, it sends a request and provides the identityToken.

    Even if you uninstall the app from the device, the identityToken remains unchanged. Therefore, it can reliably be used as a user identifier.

    Conclusions

    From a programming perspective, implementing Apple Sign-In in your apps is straightforward and enhances privacy, as users can choose whether to share their email.

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

    References

  • The MVVM-C Blueprint for iOS Apps

    The MVVM-C Blueprint for iOS Apps

    The MVVM-C pattern, which combines the Model-View-ViewModel (MVVM) architecture with a Coordinator layer, offers a structured approach to building scalable and maintainable iOS apps. It effectively separates concerns, making the codebase more modular and easier to test.

    In this tutorial, we will implement a sample focused solely on its navigation components. At the end of the post, you will find the GitHub repository where you can access the sample project used for this tutorial.

    The coordinator component

    In the MVVM-C (Model-View-ViewModel-Coordinator) pattern, the Coordinator is responsible for managing navigation and application flow, ensuring that the View and ViewModel remain focused on UI presentation and business logic, respectively, without being concerned with navigation and flow management. It handles the creation and configuration of View and ViewModel instances, determines which screen to display next based on user actions or app logic, and manages transitions between screens. By centralizing navigation logic, the Coordinator promotes modularity, reusability, and testability, maintaining a clean and scalable architecture.

    Depending on the complexity of the app, the Coordinator can be implemented in different ways:

    • Whole App Coordinator – Best for small apps with a few screens, where a single component can effectively manage the navigation flow.
    • Flow Coordinator – In larger apps, a single coordinator becomes difficult to manage. Grouping screens by business flow improves modularity and maintainability.
    • Screen Coordinator – Each screen has its own dedicated coordinator, making it useful for reusable components, such as a payment screen appearing in different user journeys. This approach is often used in architectures like VIPER, where each module operates independently.

    Ultimately, the choice of implementation depends on the app’s complexity and business requirements; no single pattern fits all use cases.

    The sample app

    The app we are going to implement is a Tab View app. Each tab represents a different navigation flow:

    flowtab1
    Screenshot

    The First Tab Flow is a flow coordinator that presents a Primary View with two buttons. These buttons navigate to either Secondary View 1 or Secondary View 2.

    • When Secondary View 2 appears, it includes a button that allows navigation to Tertiary View 1.
    • In Tertiary View 1, there is a button that returns directly to the Primary View or allows navigation back to the previous screen using the back button.
    • Secondary View 2 does not lead to any additional views; users can only return to the previous screen using the back button.

    The Second Tab Flow is managed by a Screen Coordinator, which presents a single screen with a button that opens a view model.

    • In this context, we consider the modal to be part of the view.
    • However, depending on the app’s design, the modal might instead be managed by the coordinator.

    Main Tab View

    This is the entry view point from the app:

    struct MainView: View {
        @StateObject private var tab1Coordinator = Tab1Coordinator()
        @StateObject private var tab2Coordinator = Tab2Coordinator()
    
        var body: some View {
            TabView {
                NavigationStack(path: $tab1Coordinator.path) {
                    tab1Coordinator.build(page: .primary)
                        .navigationDestination(for: Tab1Page.self) { page in
                            tab1Coordinator.build(page: page)
                        }
                }
                .tabItem {
                    Label("Tab 1", systemImage: "1.circle")
                }
    
                NavigationStack(path: $tab2Coordinator.path) {
                    tab2Coordinator.build(page: .primary)
                        .navigationDestination(for: Tab2Page.self) { page in
                            tab2Coordinator.build(page: page)
                        }
                }
                .tabItem {
                    Label("Tab 2", systemImage: "2.circle")
                }
            }
        }
    }

    The provided SwiftUI code defines a MainView with a TabView containing two tabs, each managed by its own coordinator (Tab1Coordinator and Tab2Coordinator). Each tab uses a NavigationStack bound to the coordinator’s path property to handle navigation. The coordinator’s build(page:) method constructs the appropriate views for both the root (.primary) and subsequent pages.

    The navigationDestination(for:) modifier ensures dynamic view creation based on the navigation stack, while the tabItem modifier sets the label and icon for each tab. This structure effectively decouples navigation logic from the view hierarchy, promoting modularity and ease of maintenance.

    Another key aspect is selecting an appropriate folder structure. The one I have chosen is as follows:

    Screenshot

    This may not be the best method, but it follows a protocol to prevent getting lost when searching for files.

    The flow coordinator

    The first structure we need to create is an enum that defines the screens included in the flow:

    enum Tab1Page: Hashable {
        case primary
        case secondary1
        case secondary2
        case tertiary
    }

    Hashable is not free; we need to push and pop those cases into a NavigationPath. The body of the coordinator is as follows:

    class Tab1Coordinator: ObservableObject {
        @Published var path = NavigationPath()
    
        func push(_ page: Tab1Page) {
            path.append(page)
        }
    
        func pop() {
            path.removeLast()
        }
    
        func popToRoot() {
            path.removeLast(path.count)
        }
    
        @ViewBuilder
           func build(page: Tab1Page) -> some View {
               switch page {
               case .primary:
                   Tab1PrimaryView(coordinator: self)
               case .secondary1:
                   Tab1SecondaryView1(coordinator: self)
               case .secondary2:
                   Tab1SecondaryView2()
               case .tertiary:
                   Tab1TertiaryView(coordinator: self)
               }
           }
    }

    The Tab1Coordinator class is an ObservableObject that manages navigation within a SwiftUI view hierarchy for a specific tab (Tab1). It uses a NavigationPath to track the navigation stack, allowing views to be pushed onto or popped from the stack through methods such as push(_:), pop(), and popToRoot(). The @Published property path ensures that any changes to the navigation stack are automatically reflected in the UI.

    The build(page:) method, marked with @ViewBuilder, constructs and returns the appropriate SwiftUI view (e.g., Tab1PrimaryView, Tab1SecondaryView1, Tab1SecondaryView2, or Tab1TertiaryView) based on the provided Tab1Page enum case. This approach enables dynamic navigation between views while maintaining a clean separation of concerns.

    The last section of the coordinator is the protocol implementation for the views presented by the coordinator. When a view has completed its work, it delegates the decision of which screen to present next to the coordinator. The coordinator is responsible for managing the navigation logic, not the view.

    extension Tab1Coordinator: Tab1PrimaryViewProtocol {
        func goToSecondary1() {
            push(.secondary1)
        }
        func goToSecondary2() {
            push(.secondary2)
        }
    }
    
    extension Tab1Coordinator: Tab1SecondaryView1Protocol {
        func goToTertiaryView() {
            push(.tertiary)
        }
    }
    
    extension Tab1Coordinator: Tab1TertiaryViewProtocol {
        func backToRoot() {
            self.popToRoot()
        }
    }
    

    This is the code from one of the views:

    import SwiftUI
    
    protocol Tab1PrimaryViewProtocol: AnyObject {
        func goToSecondary1()
        func goToSecondary2()
    }
    
    struct Tab1PrimaryView: View {
         let coordinator: Tab1PrimaryViewProtocol
        
            var body: some View {
                
                VStack {
                    Button("Go to Secondary 1") {
                        coordinator.goToSecondary1()
                    }
                    .padding()
    
                    Button("Go to Secondary 2") {
                        coordinator.goToSecondary2()
                    }
                    .padding()
                }
                .navigationTitle("Primary View")
            }
    }

    When the view doesn’t know how to proceed, it should call its delegate (the Coordinator) to continue.

    The screen coordinator

    The first structure we need to create is an enum that defines the screens in the flow:

    enum Tab2Page: Hashable {
        case primary
    }
    
    class Tab2Coordinator: ObservableObject {
        @Published var path = NavigationPath()
        
        @ViewBuilder
        func build(page: Tab2Page) -> some View {
            switch page {
            case .primary:
                Tab2PrimaryView(coordinator: self)
            }
        }
    }

    Hashable is not free; we need to push/pop these cases into a NavigationPath. The body of the coordinator is simply as follows:

    class Tab1Coordinator: ObservableObject {
        @Published var path = NavigationPath()
    
        func push(_ page: Tab1Page) {
            path.append(page)
        }
    
        func pop() {
            path.removeLast()
        }
    
        func popToRoot() {
            path.removeLast(path.count)
        }
    
        @ViewBuilder
           func build(page: Tab1Page) -> some View {
               switch page {
               case .primary:
                   Tab1PrimaryView(coordinator: self)
               case .secondary1:
                   Tab1SecondaryView1(coordinator: self)
               case .secondary2:
                   Tab1SecondaryView2()
               case .tertiary:
                   Tab1TertiaryView(coordinator: self)
               }
           }
    }

    The provided code defines a SwiftUI-based navigation structure for a tabbed interface. The Tab2Coordinator class is an ObservableObject that manages the navigation state using a NavigationPath, which is a state container for navigation in SwiftUI. The @Published property path allows the view to observe and react to changes in the navigation stack. The build(page:) method is a ViewBuilder that constructs the appropriate view based on the Tab2Page enum case. Specifically, when the page is .primary, it creates and returns a Tab2PrimaryView, passing the coordinator itself as a dependency.

    This approach is commonly used in SwiftUI apps to handle navigation between different views within a tab, promoting a clean separation of concerns and state management. The Tab2Page enum is marked as Hashable, which is required for it to work with NavigationPath.

    Conclusions

    Coordinator is a key component that allows to unload ViewModel or ViewModel logic for controlling navigation logic. I hope this post will help you to understand better this pattern.

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

    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