Erfassen Sie Ihre Fortschritte
Trailhead-Startseite
Trailhead-Startseite

Ändern der nativen Forceios-Anwendung

Lernziele

Nachdem Sie diese Lektion abgeschlossen haben, sind Sie in der Lage, die folgenden Aufgaben auszuführen:

  • Anpassen der REST-Anforderung der nativen iOS-Vorlagenanwendung
  • Hinzufügen einer Streichgeste und einer Schaltfläche, sodass Benutzer mithilfe der Listenansicht Salesforce-Datensätze löschen können
  • Verarbeiten der geänderten REST-Antwort

Anpassen der Listenanzeige

Die von forceios erstellte Swift-Vorlagenanwendung zeigt lediglich eine Liste mit Namen aus einer Salesforce-Organisation. Kunden haben keine Interaktionsmöglichkeiten, sondern können sich nur die Namen ansehen. Nun wollen wir eine Swift-Anwendung von forceios ein wenig interessanter machen und Unterstützung für das "Streichen nach links" hinzufügen. Bei dieser Geste reagiert Ihr Code ein und teilt iOS mit, dass es sich um eine Löschanforderung handelt. iOS fügt dann eine Schaltfläche Delete (Löschen) am Ende der Zeile hinzu, in der die Streichbewegung registriert wurde. Wenn der Benutzer auf diese Schaltfläche tippt, antwortet Ihr Code wie folgt:
  1. Wenn der Benutzer auf die Schaltfläche Delete tippt oder ganz nach links streicht:
    1. Senden Sie eine REST-Anforderung zum Löschen des zugehörigen Salesforce-Datensatzes.
    2. Entfernen Sie die Zeile aus der Listenansicht.
    3. Entfernen Sie die Daten der Zeile aus dem internen Speicher Ihrer Anwendung.
  2. Wenn die REST-Löschanforderung erfolgreich ist, laden Sie die Daten der Tabellenansicht neu.
  3. Wenn die REST-Löschanforderung fehlschlägt:
    • Weisen Sie den Benutzer auf den Fehler hin.
    • Fügen Sie die entfernte Zeile wieder in die Listenansicht und in den internen Speicher Ihrer Anwendung ein.

Ändern der REST-Standardanforderung

Die Tabellenansicht Ihrer Anwendung bezieht ihre Werte über eine einfache SOQL SELECT-Abfrage. Standardmäßig werden bei dieser Abfrage Benutzerdatensätze angefordert. Mit dem Datensatznamen "User" können Sie jedoch nicht viel anfangen, daher wollen wir "User" in "Contact" ändern.

  1. Öffnen Sie in Xcode die Arbeitsumgebung "MyTrailNativeApp" (sofern diese nicht noch geöffnet ist).
  2. Wählen Sie im Projektnavigator die Datei RootViewController.swift aus.
  3. Blättern Sie zur loadView()-Methode.
  4. Ändern Sie die SOQL-Abfrage wie folgt:
    SELECT Name, Id FROM Contact

Hinzufügen der Löschschaltflächen-Protokollmethoden

Um die Streichbewegung zu erfassen und die Schaltfläche Delete hinzuzufügen, überschreiben Sie eine Reihe von UITableViewController-Methoden.

  1. Fügen Sie der RootViewController-Klasse "Stubs" für diese UITableViewController-Methoden ein.
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    
    }
    
    override func tableView(_ tableView: UITableView, 
        commit editingStyle: UITableViewCell.EditingStyle, 
        forRowAt indexPath: IndexPath) {
    
    }
  2. Fügen Sie den folgenden vorgefertigten Implementierungscode für die erste dieser Methoden hinzu:
    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
        }
    }

Wir wollen nun analysieren, was in diesem Code passiert. Wenn iOS die Geste "Streichen nach links" des Benutzers erkennt, ruft es die Methode tableView(_:editingStyleForRowAt:) auf, um festzustellen, welche Art von Bearbeitung der Benutzer wünscht. Um anzugeben, dass es sich um eine Löschanforderung handelt, geben Sie UITableViewCellEditingStyle.delete zurück. Beim Streichen nach links zeigt iOS dann am rechten Zeilenende eine Schaltfläche Löschen an.

Natürlich sollten Sie nicht versuchen, etwas zu löschen, das gar nicht da ist. Wie erkennen Sie, ob die Streichbewegung auf einer leeren Zeile erfolgte? Sie können die Anzahl der Datensätze in den Daten mit dem nullbasierten Index der Zeile vergleichen, über die gestrichen wurde. Ist der Index größer oder gleich der Anzahl von Datensätzen, ist die Zeile leer. Sie geben dann UITableViewCellEditingStyle.none zurück, um iOS anzuweisen, die Anforderung zu ignorieren.

Beachten Sie, dass der Code den count-Wert aus dem dataRows-Objekt liest. In der Vorlagenanwendung definiert RootViewController dieses Objekt, um Datensätze aufzunehmen, die durch erfolgreiche REST-Antworten zurückgegeben wurden. Erinnern Sie sich, wie RootViewController mithilfe von dataRows die Tabellenansicht auffüllt?

Einrichten der benutzerdefinierten Bearbeitungsaktion

Die zweite UITableViewController-Überschreibungsmethode führt benutzerdefinierte Aktionen für den angegebenen Bearbeitungsvorgang durch. In dieser Methode erstellen und senden Sie eine REST-Anforderung zum Löschen des Salesforce-Datensatzes hinter der Zeile, auf der die Streichbewegung erfolgte. Da der REST-Mechanismus asynchron ausgeführt wird, wissen Sie nach dem Erhalt der Antwort, ob die Anforderung erfolgreich war oder fehlgeschlagen ist. Sie können jedoch eine "guard"-Anweisung als Schutzmaßnahme gegen Fehler verwenden.

Es besteht die Möglichkeit, dass ein übereifriger Benutzer mehrere Zeilen löscht, bevor die erste REST-Antwort ankommt. Wie können Sie die gelöschte Zeile erneut instanziieren, wenn eine der Anforderungen fehlschlägt? Eine Möglichkeit besteht darin, Informationen zu jeder gelöschten Zeile in einem Datenverzeichnis zwischenzuspeichern. Damit dieses Datenverzeichnis über Methodenaufrufe hinweg verfügbar ist, können Sie eine Eigenschaft erstellen.

  1. Deklarieren Sie im oberen Teil der RootViewController-Klasse das folgende struct und eine Variable, die es verwendet.
    class RootViewController : UITableViewController {
        var dataRows = [Dictionary<String, Any>]()
        
        struct DeletedItemInfo {
            var data: [String:Any]
            var path: IndexPath
        }
        private var deleteRequests = [Int:DeletedItemInfo]()
    
  2. Deklarieren Sie in der Methode tableView(_:commit:forRowAt:) eine row-Variable, um den ursprünglichen Speicherort des gelöschten Elements nachzuverfolgen.
    let row = indexPath.row
  3. Fügen Sie einen if-Block hinzu. Stellen Sie für die if-Bedingungen sicher, dass der Bearbeitungsstil einen Löschvorgang angibt, und verwenden Sie außerdem den row < count-Test.
    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) {
    
        }
    }
    In diesem Bedingungsblock können Sie Code hinzufügen, um die Wiederherstellungsdaten zwischen zu speichern.
  4. Rufen Sie im if-Block die ID des ausgewählten Datensatzes aus dem zugehörigen dataRows-Objekt ab.
    let deletedId: String = self.dataRows[row]["Id"] as! String
  5. Erstellen Sie ein konstantes DeletedItemInfo-Objekt namens deletedItemInfo und speichern Sie die ausgewählte Zeile sowie den zugehörigen Indexpfad darin. (Später fügen Sie dieses Objekt zum Datenverzeichnis deleteRequests hinzu.)
    let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)

Hier sehen Sie den if-Block im aktuellen Zustand:

    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)
    }

Löschen des Eintrags aus Salesforce, der Tabellenansicht und der Datenquelle

In der Listenanzeige müssen jetzt nur noch eine Anforderung zum Löschen (DELETE) an den Server gesendet werden.

Beim Erstellen der REST-Anforderung verwenden Sie ihre ID als Schlüssel für ein Objekt im deleteRequests-Datenverzeichnis. Sie können die gleiche ID später wiederverwenden, um Informationen zum gelöschten Element abzurufen.

iOS schreibt hier ein wichtiges Protokoll vor: Wenn Sie eine Zeile aus einer Tabellenansicht löschen, müssen Sie den entsprechenden Eintrag aus der Datenquelle löschen, und zwar im selben Codeblock. Daher verfügen Sie innerhalb dieser Methode sowohl über den Zeilen- als auch über den Datenquelleintrag.

  1. Geben Sie nach dem vorhergehenden if-Block eine Konstante ein, die auf die freigegebene Instanz von RestClient verweist.
    let restClient = RestClient.shared
  2. Erstellen Sie ein RestRequest-Objekt zum Löschen des Salesforce-Objekts. Zum Erstellen der Anforderung rufen Sie die Methode restClient.requestForDelete auf und verwenden dabei den Objekttyp "Kontakt" und den deletedId-Wert. Diese beiden Werte geben den zu löschenden Salesforce-Datensatz genau an.
    let deleteRequest =
        restClient.requestForDelete(withObjectType: "Contact", 
            objectId: deletedId, apiVersion: nil)
  3. Für die Übertragung der Anforderung an Salesforce rufen Sie die RestClient-Funktion send(request:_:) auf. Der nachgeschaltete Abschluss tritt an die Stelle des zweiten Arguments und kann entweder den Antworttyp "Erfolg" oder "Fehler" verarbeiten. Um die Notwendigkeit von Optionals in diesem asynchronen Abschluss zu umgehen, verwenden Sie eine guard-Anweisung, um sicherzustellen, dass sich self weiterhin im Arbeitsspeicher befindet.
    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. Im Fall .success aktualisieren wir die Liste der Kontakte der Anwendung. Fügen Sie zuerst einen Block hinzu, der im Haupt-Thread ausgeführt wird. Sie verwenden solche Blöcke für alle UI-Aktualisierungen, die asynchron erfolgen.
    case .success(_):
        DispatchQueue.main.async {
    
        }
    }
  5. Speichern Sie im Block DispatchQueue.main.async das Array deletedItemInfo im Datenverzeichnis deleteRequests zwischen. Verwenden Sie die ID des RestRequest-Objekts (request.hashValue) als Schlüssel.
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                    
        }
    }
  6. Um die Anzeige zu aktualisieren, löschen Sie die Zeile aus dem dataRows-Objekt und laden die Tabellenansicht neu. Da wir in diesem Fall response nicht verwenden, ersetzen Sie let response durch _.
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
            strongSelf.dataRows.remove(at: row)
            strongSelf.tableView.reloadData()
        }
  7. Protokollieren Sie im Fall .failure eine Fehlermeldung. Wir kehren später zu diesem Block zurück, um die Fehlerbehandlung zu verbessern.
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
    
Jetzt sind Sie fertig. So sehen die Methoden tableView(_:editingStyleForRowAt:) und tableView(_:commit:forRowAt:) zu diesem Zeitpunkt aus.
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)
            }
        }
    }
}

Vollenden des Werks

Hervorragend! Sie haben Ihrer neuen Anwendung einige Benutzerinteraktionen hinzugefügt. Es ist bloß eine Schaltfläche, über die ein Datensatz auf dem Server gelöscht wird; aber für diese schlichte Vorlage ist das ein Riesenschritt. Widmen wir uns nun dem Problem der Fehlerbehandlung, also den Schritten, die erfolgen sollen, wenn unsere Löschanforderung nicht funktioniert.

In dem Augenblick, in dem eine Fehlerbenachrichtigung eintrifft, haben Sie den gelöschten Datensatz bereits sowohl aus dem dataRows-Array als auch aus der Tabellenansicht entfernt. Natürlich müssen Sie diese Elemente zurückverfolgen und neu instanziieren – aber wie? Zum Glück sind Sie darauf vorbereitet. Erinnern Sie sich daran, wie Sie jedes DELETE REST-Anforderungsobjekt im deleteRequests-Datenverzeichnis zwischengespeichert haben? Clever gedacht! Da jeder REST-Fehler das betreffende RestRequest-Objekt zurückgibt, finden Sie den gelöschten Inhalt im deleteRequests-Datenverzeichnis problemlos.

Definieren einer reinstateDeleteRowWithRequest(_:) Methode

  1. Fügen Sie am Ende der RootViewController-Klassendefinition die Methode reinstateDeleteRowWithRequest(_: RestRequest) mit einem leeren Implementierungsblock hinzu.
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
    
    }
  2. Da diese Methode UI-Elemente ändert, beginnen Sie mit einem Block, der sicherstellt, dass alle Aufrufe im Haupt-Thread erfolgen.
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
        DispatchQueue.main.async {
            
        }
    }
  3. Verwenden Sie im Block DispatchQueue.main.async die ID des Anforderungsarguments, um die gelöschten Daten im deleteRequests-Datenverzeichnis nachzuschlagen. Der Rest der Methode wird nur angewandt, wenn dieser Wert nicht Null ist.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
    
        }
    }
  4. Wenn die Werte der gelöschten Zeile erfolgreich abgerufen wurden, fügen Sie das Datenobjekt an Indexposition 0 des dataRows-Datenverzeichnisses ein.
    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. Um den gelöschten Kontaktnamen wieder herzustellen, laden Sie die Daten der Ansicht UITableView neu.
    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. Da das Löschen des Kontakts rückgängig gemacht wurde, wird der zugehörige Eintrag aus dem deleteRequests-Array entfernt.
    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)
        }
    }

    Die endgültige reinstateDeleteRowWithRequest(_:)-Methode sieht wie folgt aus:

    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)
            }
        }
    }

Verarbeiten von REST-Fehlerantworten

Eine REST-Antwort kann Erfolg oder Fehlschlag anzeigen. Erfolg heißt in unserem Fall, dass die Zeile erfolgreich gelöscht wurde. Fehler bedeutet, dass die Zeile nicht gelöscht werden konnte oder die Verbindung abgebrochen bzw. wegen Timeout beendet wurde. In diesen Fällen meldet das Netzwerk einen Fehler.

Sie können jetzt ihre neue reinstateDeleteRowWithRequest(_:)-Methode verwenden, indem Sie sie aufrufen, wenn eine REST-Antwort einen Fehler anzeigt. In dieser Swift-Anwendung verwenden Sie den Funktionsabschluss onFailure der RestClient.send()-Methode, um REST-Fehlerantworten zu verarbeiten. Dieser Block ersetzt die RestClientDelegate-Fehlerhandler von Objective-C.

Für diese Anwendung rufen Sie unabhängig vom Fehlertyp reinstateDeleteRowWithRequest(_:) auf. Außerdem zeigen Sie dem Benutzer die Fehlermeldung in einem Warnfeld an. Damit der Warnfeldcode wiederverwendbar wird, platzieren wir ihn in einem eigenen Methodenhauptteil.

  1. Erstellen Sie im Hauptteil der RootViewController-Klasse eine neue private Methode namens showErrorAlert. Diese Methode verwendet ein Argument: das Objekt RestClientError, das an den catch-Block übergeben wird.
    private func showErrorAlert(_ error: RestClientError) {
    
    }
  2. Im DispatchQueue.main.async-Block erstellen Sie das Warnfeld und zeigen Sie es an. Für die Zeichenfolge des Warnfelds verwenden Sie das Member message des Arrays error._userInfo. Dieser Code sieht etwas ungeordnet aus. Dies liegt daran, dass userInfo von der Anwendung definiert wird und optional ist. Bevor Sie den Code verwenden, sollten Sie sicherstellen, dass der festgestellte Fehler ein userInfo-Array mit der erwarteten Struktur enthält.
    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. Rufen Sie in der tableView(_:commit:forRowAt:)-Methode die neuen Methoden im Fall .failure der RestClient send()-Methode auf.
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
        strongSelf.reinstateDeletedRowWithRequest(deleteRequest)
        strongSelf.showErrorAlert(error as RestClientError)
    }
    Die endgültige tableView(_:commit:forRowAt:)-Methode sieht wie folgt aus.
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)
            }
        }
    }
}

Probieren Sie es aus!

Geschafft! Sie sind nun bereit, den Build für Ihren Code im iOS-Simulator zu erstellen und auszuführen. Achten Sie darauf: Für jeden Standardkontakt in der Developer Edition-Datenbank, den Sie zu löschen versuchen, wird eine Fehlerantwort angezeigt. Diese Fehler treten auf, weil jeder Kontakt, der bereits in einer Developer Edition-Organisation vordefiniert ist, das übergeordnete Element anderer Datensätze ist. Zur Vorbereitung auf die Tests müssen Sie sich bei Ihrer Developer Edition-Organisation anmelden und einen oder mehrere Kontakte erstellen, die keine anderen Datensätze besitzen.

Ressourcen