Suivez votre progression
Accueil Trailhead
Accueil Trailhead

Modification de l’application native Forceios

Objectifs de formation

Une fois cette unité terminée, vous pourrez :

  • Personnaliser la requête REST dans l'application modèle iOS native
  • Ajouter un geste de balayage et un bouton que les utilisateurs peuvent utiliser dans la vue de liste pour supprimer des enregistrements Salesforce
  • Gérer la réponse REST modifiée

Personnalisation de l'écran de la liste

L’application modèle Swift créée par forceios affiche simplement une liste de noms provenant d’une organisation Salesforce. Les clients ne peuvent pas interagir avec la liste ; ils peuvent seulement consulter les noms. Nous allons agrémenter une application Swift forceios en ajoutant la prise en charge d’un geste de balayage vers la gauche. Lorsque ce geste est effectué, votre code l’intercepte et signale une requête de suppression à iOS. iOS ajoute ensuite un bouton Supprimer à droite de la ligne balayée. Si l’utilisateur touche ce bouton, votre code répond comme suit :
  1. Lorsque l’utilisateur appuie sur le bouton Supprimer ou effectue un balayage vers la gauche :
    1. Il envoie une requête REST pour supprimer l’enregistrement Salesforce associé.
    2. Il supprime la ligne de la vue de liste.
    3. Il supprime les données de la ligne du stockage interne de votre application.
  2. Si la requête de suppression REST réussit, il recharge les données de la vue du tableau.
  3. Si la requête de suppression REST échoue :
    • Il signale l’erreur à l’utilisateur.
    • Il rétablit la ligne supprimée dans la vue de liste et dans le stockage interne de votre application.

Modification de la requête REST par défaut

La vue du tableau de votre application reçoit ses valeurs d’une simple requête SOQL SELECT. Par défaut, cette requête demande les enregistrements Utilisateur. Vous ne pouvez exécuter d’actions avec les enregistrements Utilisateur. Par conséquent, changeons Utilisateur en Contact.

  1. Dans Xcode, ouvrez l’espace de travail « MyTrailNativeApp » s’il n’est pas déjà ouvert.
  2. Dans le navigateur du projet, sélectionnez le fichier RootViewController.swift.
  3. Accédez à la méthode loadView().
  4. Remplacez la SOQL par la suivante :
    SELECT Name, Id FROM Contact

Ajout des méthodes de protocole du bouton Supprimer

Pour intercepter le balayage et ajouter le bouton Supprimer, vous devez remplacer deux méthodes UITableViewController.

  1. Dans la classe RootViewController, ajoutez des « bouts » à ces méthodes UITableViewController.
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    
    }
    
    override func tableView(_ tableView: UITableView, 
        commit editingStyle: UITableViewCell.EditingStyle, 
        forRowAt indexPath: IndexPath) {
    
    }
  2. Ajoutez le code d'implémentation standard suivant pour la première de ces méthodes :
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        if indexPath.row < self.dataRows.count {
           return UITableViewCell.EditingStyle.delete
        } else {
           return UITableViewCell.EditingStyle.none
        }
    }

Analysons ce qui se passe dans ce code. Quand iOS détecte le geste de balayage vers la gauche de l’utilisateur, il appelle la méthode tableView(_:editingStyleForRowAt:) pour déterminer le type de modification voulu par l’utilisateur. Pour indiquer une requête de suppression, renvoyez UITableViewCellEditingStyle.delete. Lorsqu’un balayage vers la gauche est effectué, iOS affiche un bouton Supprimer à droite de la ligne.

Bien entendu, n’essayez pas de supprimer des données absentes. Comment savoir si le balayage est appliqué à une ligne vide ? Vous pouvez comparer le nombre d’enregistrements dans le jeu de données à l’index de base zéro de la ligne balayée. Si l’index est supérieur ou égal au nombre d’enregistrements, la ligne est vide. Ensuite, renvoyez UITableViewCellEditingStyle.none pour indiquer à iOS d’ignorer la requête.

Le code lit count à partir de l’objet dataRows. Dans l’application modèle, RootViewController définit cet objet pour contenir les enregistrements renvoyés par les réponses REST réussies. Vous souvenez-vous comment RootViewController utilise dataRows pour renseigner la vue du tableau

Configuration de l'action de modification personnalisée

La deuxième méthode de remplacement UITableViewController exécute des actions personnalisées pour l’opération de modification que vous avez spécifiée. Dans cette méthode, vous créez et envoyez une requête REST pour supprimer l’enregistrement Salesforce correspondant à la ligne balayée. Comme le mécanisme REST fonctionne de manière asynchrone, vous pouvez uniquement savoir si la requête a réussi ou échoué lorsque la réponse arrive. Cependant, il est possible d’éviter ce type d’erreur.

Un utilisateur peut supprimer par erreur plusieurs lignes avant de recevoir la première réponse REST. Si une requête échoue, comment pouvez-vous rétablir la ligne supprimée ? Une méthode consiste à mettre en cache les informations de chaque ligne supprimée dans un dictionnaire. Pour que ce dictionnaire soit accessible dans tous les appels de méthode, vous pouvez créer une propriété.

  1. En haut de la classe RootViewController, déclarez la structure struct suivante et une variable qui l’utilise.
    class RootViewController : UITableViewController {
        var dataRows = [Dictionary<String, Any>]()
        
        struct DeletedItemInfo {
            var data: [String:Any]
            var path: IndexPath
        }
        private var deleteRequests = [Int:DeletedItemInfo]()
    
  2. Dans la méthode tableView(_:commit:forRowAt:), déclarez une variable row pour suivre l’emplacement d’origine de l’élément supprimé.
    let row = indexPath.row
  3. Ajoutez un bloc if. Pour les conditions if, assurez-vous que le style de modification indique une suppression et utilisez également le test row < count.
    override func tableView(_ tableView: UITableView, 
        commit editingStyle: UITableViewCell.EditingStyle, 
        forRowAt indexPath: IndexPath) {
        let row = indexPath.row
        if (row < self.dataRows.count) && 
            (editingStyle == UITableViewCell.EditingStyle.delete) {
    
        }
    }
    Ajoutez le code dans ce bloc de conditions pour mettre en cache les données de récupération.
  4. Dans le bloc if, vous pouvez obtenir l'ID de l'enregistrement sélectionné depuis l'objet dataRows associé.
    let deletedId: String = self.dataRows[row]["Id"] as! String
  5. Créez un objet DeletedItemInfo constant, intitulé deleteItemInfo, et stockez-y la ligne sélectionnée ainsi que le chemin de son index. (Plus tard, vous ajouterez cet objet au dictionnaire deleteRequests.)
    let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)

Voici le bloc if dans son état actuel :

    if (row < self.dataRows.count) && 
        (editingStyle == UITableViewCell.EditingStyle.delete) {
        let deletedId: String = self.dataRows[row]["Id"] as! String
        let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)
    }

Suppression de l’entrée depuis Salesforce, de la vue du tableau et de la source de données

Dans l’écran de la liste, il vous suffit ensuite d’envoyer une requête DELETE au serveur.

Lorsque vous créez la requête REST, vous utilisez son ID en tant que clé d'objet dans le dictionnaire deleteRequests. Vous pouvez ensuite réutiliser le même ID pour récupérer les informations de l'élément supprimé.

iOS applique ici un protocole important : si vous supprimez une ligne à partir d’une vue du tableau, vous devez supprimer l’entrée correspondante de la source de données dans le même bloc de code. Vous disposez ainsi de la ligne et de l’entrée de la source de données au sein de cette méthode.

  1. À la suite du bloc if précédent, indiquez une constante qui fait référence à l’instance partagée de RestClient.
    let restClient = RestClient.shared
  2. Créez un objet RestRequest pour supprimer l’objet Salesforce. Pour créer la requête, appelez la méthode restClient.requestForDelete à l’aide du type d’objet « Contact » et de la valeur deletedId. Ces deux valeurs indiquent précisément l’enregistrement Salesforce à supprimer.
    let deleteRequest =
        restClient.requestForDelete(withObjectType: "Contact", 
            objectId: deletedId, apiVersion: nil)
  3. Pour envoyer la requête à Salesforce, appelez la fonction RestClient send(request:_:). La closure finale remplace le deuxième argument et peut gérer le type de réponse réussite ou échec. Pour éviter de devoir utiliser des options dans cette closure asynchrone, utilisez une instruction guard pour garantir que self est toujours en mémoire.
    let restClient = RestClient.shared
    let deleteRequest =restClient.requestForDelete(withObjectType: "Contact", 
            objectId: deletedId, apiVersion: nil)
    restClient.send(request: deleteRequest){ [weak self] (result) in
        guard let strongSelf = self else {
            return
        }
        switch result {
            case .success(let response):
    
            case .failure(let error):
    
        }
    }
  4. En cas de .success, nous mettons à jour la liste de contacts de l’application. Commencez par ajouter un bloc qui s’exécute sur le thread principal. Utilisez un bloc de ce type pour toutes les mises à jour de l’interface utilisateur effectuées de manière asynchrone.
    case .success(_):
        DispatchQueue.main.async {
    
        }
    }
  5. Dans le bloc DispatchQueue.main.async, mettez le tableau deletedItemInfo en cache dans le dictionnaire deleteRequests. Utilisez l’ID de l’objet RestRequest, request.hashValue, comme clé.
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                    
        }
    }
  6. Pour mettre à jour l’affichage, supprimez la ligne de l’objet dataRows et rechargez la vue du tableau. Étant donné que nous n’utilisons pas response dans cette requête, remplacez let response avec _.
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
            strongSelf.dataRows.remove(at: row)
            strongSelf.tableView.reloadData()
        }
  7. En cas de .failure, enregistrez un message d'erreur. Nous reviendrons à ce bloc ultérieurement pour améliorer la gestion des erreurs.
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
    
Vous avez terminé ! Voici les méthodes tableView(_:editingStyleForRowAt:) et tableView(_:commit:forRowAt:) jusqu’à ce point.
override func tableView(_ tableView: UITableView, 
    editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    if indexPath.row < self.dataRows.count {
       return UITableViewCell.EditingStyle.delete
    } else {
       return UITableViewCell.EditingStyle.none
    }
}

override func tableView(_ tableView: UITableView, 
    commit editingStyle: UITableViewCell.EditingStyle, 
    forRowAt indexPath: IndexPath) {
    let row = indexPath.row
    if (row < self.dataRows.count) && 
        (editingStyle == UITableViewCell.EditingStyle.delete) {
        let deletedId: String = self.dataRows[row]["Id"] as! String
        let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)
        let restClient = RestClient.shared
        
        let deleteRequest =
            restClient.requestForDelete(withObjectType: "Contact", 
        objectId: deletedId, apiVersion: nil)
        restClient.send(request: deleteRequest) { [weak self] (result) in
            guard let strongSelf = self else {
                return
            }
            switch result {
                case .success(_):
                    DispatchQueue.main.async {
 .                       strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                         strongSelf.dataRows.remove(at: row)
                         strongSelf.tableView.reloadData()
                     }
                case .failure(let error):
                    os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
            }
        }
    }
}

Ajout des touches finales

Très bien ! Vous avez ajouté quelques interactions utilisateur à votre nouvelle application. Il s’agit d’un simple bouton qui supprime un enregistrement du serveur, mais pour ce modèle vide, le changement est considérable. Maintenant, abordons le problème de la gestion des échecs. Nous n’allons pas discuter ici de nos échecs existentiels, bien entendu, mais plutôt de la manière de procéder à une restauration si notre demande de suppression ne fonctionne pas.

Vous venez d’apprendre que la suppression a échoué, mais vous avez déjà effacé l’enregistrement supprimé du tableau dataRows et de la vue du tableau. Vous devez faire marche arrière et rétablir ces éléments, mais comment procéder ? Heureusement, vous avez tout préparé. Rappelez-vous, vous avez mis en cache chaque objet de requête REST DELETE dans le dictionnaire deleteRequests. Bien pensé ! Comme chaque erreur REST renvoie l’objet RestRequest pertinent, vous pouvez facilement rechercher le contenu supprimé dans le dictionnaire deleteRequests.

Définition d’une méthode reinstateDeleteRowWithRequest(_:) Méthode

  1. À la fin de la définition de la classe RootViewController, ajoutez une méthode reinstateDeleteRowWithRequest(_: RestRequest) avec un bloc d’implémentation vide.
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
    
    }
  2. Dans la mesure où cette méthode modifie les éléments de l’interface utilisateur, utilisez un bloc qui garantit que tous les appels ont lieu sur le thread principal.
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
        DispatchQueue.main.async {
            
        }
    }
  3. Dans le bloc DispatchQueue.main.async, utilisez l’ID de l’argument de la requête pour chercher les données supprimées dans le dictionnaire deleteRequests. Le reste de cette méthode s’applique seulement si cette valeur n’est pas nulle.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
    
        }
    }
  4. Si vous avez bien récupéré les valeurs de la ligne supprimée, réinsérez l'objet de donnée à l'indice 0 du dictionnaire dataRows.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValues.data, at: 0)
    
        }
    }
  5. Pour rétablir le nom du contact supprimé, rechargez les données de UITableView.
    if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValues.data, at: 0)
            self.tableView.reloadData()
    
        }
    }
  6. Le contact a été restauré, vous pouvez retirer cette entrée du tableau deleteRequests.
    if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValues.data, at: 0)
            self.tableView.reloadData()
            self.deleteRequests.removeValue(forKey: request.hashValue as Int)
        }
    }

    Voici la méthode reinstateDeleteRowWithRequest(_:) finalisée :

    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
        DispatchQueue.main.async {
            if let rowValues = self.deleteRequests[request.hashValue] {
                // the beginning of the dataRows dictionary (index 0).
                self.dataRows.insert(rowValues.data, at: 0)
                self.tableView.reloadData()
                self.deleteRequests.removeValue(forKey: request.hashValue as Int)
            }
        }
    }

Gestion des erreurs de réponse REST

Une réponse REST peut indiquer une réussite ou un échec. Dans notre cas, la réussite signifie que la ligne a été supprimée. L’échec signifie que la ligne n’a pas pu être supprimée, ou que la connexion a été interrompue ou a expiré. Dans ces cas, le réseau répond par une erreur.

Vous pouvez maintenant utiliser votre nouvelle méthode reinstateDeleteRowWithRequest(_:) en l’appelant lorsqu’une réponse REST signale une erreur. Dans cette application Swift, vous utilisez la closure onFailure de la méthode RestClient.send() pour gérer les erreurs de réponse REST. Ce bloc remplace les trois gestionnaires d’erreur Objective-C RestClientDelegate.

Pour cette application, vous appelez reinstateDeleteRowWithRequest(_:), quel que soit le type de l’erreur. Le message d’erreur est également affiché à l’utilisateur dans un message d’alerte. Pour rendre le code du message d’alerte réutilisable, nous allons le placer dans le corps même de la méthode.

  1. Dans le corps de la classe RootViewController, créez une méthode privée appelée showErrorAlert. Cette méthode accepte un argument : l’objet RestClientError qui est transmis au bloc catch.
    private func showErrorAlert(_ error: RestClientError) {
    
    }
  2. Dans le bloc DispatchQueue.main.async, créez et affichez un message d’alerte. Pour la chaîne du message d’alerte, utilisez le membre message du tableau error._userInfo. Si ce code semble un peu confus, c’est parce que userInfo est facultatif et défini par l’application. Avant de l’utiliser, assurez-vous que l’erreur rencontrée contient un tableau userInfo avec la structure attendue.
    private func showErrorAlert(_ error: RestClientError) {
        var message:String = ""
        switch error {
            case .apiInvocationFailed(let underlyingError, _):
                let errArray = underlyingError._userInfo?["error"] as! [Any]
                if errArray.count > 0 {
                    let dictionary =  errArray[0] as! [String:Any]
                    message = (dictionary["message"] as? String) ??
                                "Failed to delete item"
                }
            case .apiResponseIsEmpty:
                break
            case .decodingFailed(_):
                message = "Decoding error"
            case .jsonSerialization(_):
                message = "Serialization error"
        }    
            
        let title = "Cannot delete item"
        let alert = UIAlertController(title: title, message:
            message, preferredStyle: .alert)
        DispatchQueue.main.async {
            alert.addAction(
                UIAlertAction(title: NSLocalizedString("OK", comment: ""),
                style: UIAlertAction.Style.default, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
    }
  3. Dans la méthode tableView(_:commit:forRowAt:), appelez les nouvelles méthodes de la requête .failure de la méthode RestClient send().
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
        strongSelf.reinstateDeletedRowWithRequest(deleteRequest)
        strongSelf.showErrorAlert(error as RestClientError)
    }
    Voici la méthode tableView(_:commit:forRowAt:) finale.
override func tableView(_ tableView: UITableView, 
    commit editingStyle: UITableViewCell.EditingStyle, 
    forRowAt indexPath: IndexPath) {
    let row = indexPath.row
    if (row < self.dataRows.count) && 
        (editingStyle == UITableViewCell.EditingStyle.delete) {
        let deletedId: String = self.dataRows[row]["Id"] as! String
        let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)
        let restClient = RestClient.shared
        
        let deleteRequest =
            restClient.requestForDelete(withObjectType: "Contact", 
        objectId: deletedId, apiVersion: nil)
        restClient.send(request: deleteRequest) { [weak self] (result) in
            guard let strongSelf = self else {
                return
            }
            switch result {
                case .success(_):
                    DispatchQueue.main.async {
                    strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                        strongSelf.dataRows.remove(at: row)
                        strongSelf.tableView.reloadData()
                    }
                case .failure(let error):
                    os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
                    strongSelf.reinstateDeletedRowWithRequest(deleteRequest)
                    strongSelf.showErrorAlert(error as RestClientError)
            }
        }
    }
}

Exercez-vous !

Nous avons terminé ! Vous pouvez créer et exécuter votre code dans le simulateur iOS. Notez que vous recevez une réponse d’erreur lorsque vous essayez de supprimer un contact par défaut dans la base de données Developer Edition. Ces erreurs se produisent car chaque contact pré-empaqueté dans une organisation Developer Edition est le parent d’autres enregistrements. Pour préparer le test, connectez-vous à votre organisation Developer Edition, puis créez un ou plusieurs contacts test qui n’ont pas d’autres enregistrements.

Ressources