進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

forceios ネイティブアプリケーションの変更

学習の目的

この単元を完了すると、次のことができるようになります。

  • iOS ネイティブテンプレートアプリケーションの REST 要求をカスタマイズする。
  • スワイプ操作とボタンを追加し、ユーザがリストビューを使用して Salesforce レコードを削除できるようにする。
  • 変更された REST 応答を処理する。

リスト画面のカスタマイズ

forceios で作成された Swift テンプレートアプリケーションは、Salesforce 組織から名前のリストのみを表示します。顧客がこのリストを操作することはできません。名前を参照するだけです。そこで、左スワイプ操作のサポートを追加して forceios Swift アプリケーションに変化を付けましょう。この操作が発生すると、コードがインターセプトして iOS にそれが削除要求を示すことを伝えます。これを受けて iOS は、スワイプされた行の右端に [削除] ボタンを追加します。ユーザがこのボタンをタップすると、コードは次のように反応します。
  1. ユーザが [削除] ボタンをタップする、または左端にスワイプした場合、次の操作を実行します。
    1. 関連付けられた Salesforce レコードを削除するための REST 要求を送信する。
    2. リストビューから行を削除する。
    3. 行のデータをアプリケーションの内部ストレージから削除する。
  2. REST 削除要求が成功したら、テーブルビューのデータを再読み込みします。
  3. REST 削除要求が失敗したら、次の操作を行います。
    • ユーザにエラーをアラート通知する。
    • 削除した行をリストビューとアプリケーションの内部ストレージに戻す。

デフォルトの REST 要求を変更する

アプリケーションのテーブルビューは、その値を簡単な SOQL SELECT クエリから取得します。デフォルトでは、このクエリは User レコードを要求します。ただし、User レコードでできる処理は多くないため、User を Contact に変更しましょう。

  1. Xcode で、「MyTrailNativeApp」ワークスペースがまだ開いていない場合は、開きます。
  2. プロジェクトナビゲータで、RootViewController.swift ファイルを選択します。
  3. loadView() メソッドまでスクロールします。
  4. SOQL クエリを次のように変更します。
    SELECT Name, Id FROM Contact

[削除] ボタンプロトコルメソッドを追加する

スワイプをインターセプトして [削除] ボタンを追加するには、2 つの UITableViewController メソッドを上書きします。

  1. RootViewController クラスで、これらの UITableViewController メソッドの「スタブ」を挿入します。
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    
    }
    
    override func tableView(_ tableView: UITableView, 
        commit editingStyle: UITableViewCell.EditingStyle, 
        forRowAt indexPath: IndexPath) {
    
    }
  2. 最初の方のメソッドに、次の定型の実装コードを追加します。
    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
        }
    }

このコードで何が起こるかを分析しましょう。iOS は、ユーザの左スワイプ操作を検出すると、tableView(_:editingStyleForRowAt:) メソッドをコールしてユーザが要求する編集の種類を調べます。削除要求を指示するには、UITableViewCellEditingStyle.delete を返します。これで iOS は左スワイプ時に行の右端に [削除] ボタンを表示します。

当然、ないものに削除を試みるようなことはしたくありません。スワイプが空の行に対して行われたことを検出するにはどうすればよいでしょうか? データセット内のレコード数と、スワイプされた行のゼロベースのインデックスを比較できます。インデックスがレコード数以上であれば、行は空です。その場合は、UITableViewCellEditingStyle.none を返して、iOS に要求を無視するように指示します。

コードは dataRows オブジェクトの count を読み取ります。テンプレートアプリケーションでは、RootViewController が REST の成功応答で返されたレコードを保持するためにこのオブジェクトを定義しています。RootViewControllerdataRows を使用してテーブルビューに入力する方法を覚えていますか?

カスタム編集アクションを設定する

2 番目の UITableViewController 上書きメソッドでは、指定した編集操作のカスタムアクションを実行します。このメソッドでは、スワイプされた行に表示されている Salesforce レコードを削除するための REST 要求を作成して送信します。REST のメカニズムは非同期に実行されるため、要求が成功したか失敗したかは、応答が届くまでわかりません。ただし、失敗に備えることはできます。

最初の REST 応答を受信する前に、操作のすばやいユーザが複数の行を削除する可能性があります。いずれかの要求が失敗したら、削除された行をどうやって元に戻せるでしょうか? 削除された行ごとに情報を辞書にキャッシュしておくという方法があります。この辞書を複数のメソッドコールにわたってアクセスできるようにするために、プロパティを作成できます。

  1. RootViewController クラスの最上部付近で、次の struct と、これを使用する変数を宣言します。
    class RootViewController : UITableViewController {
        var dataRows = [Dictionary<String, Any>]()
        
        struct DeletedItemInfo {
            var data: [String:Any]
            var path: IndexPath
        }
        private var deleteRequests = [Int:DeletedItemInfo]()
    
  2. tableView(_:commit:forRowAt:) メソッドでは、row 変数を宣言し、削除された項目の元の場所を追跡します。
    let row = indexPath.row
  3. if ブロックを追加します。if 条件で、編集スタイルが削除になっていることを確認し、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) {
    
        }
    }
    この条件ブロックに、回復データをキャッシュするコードを追加します。
  4. if ブロック内で、選択したレコードの ID を、関連する dataRows オブジェクトから取得します。
    let deletedId: String = self.dataRows[row]["Id"] as! String
  5. deletedItemInfo という名前の DeletedItemInfo 定数オブジェクトを作成し、選択した行とそのインデックスパスを保存します。(後で、このオブジェクトを deleteRequests 辞書に保存します)。
    let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)

if ブロックの現在の状態は次のようになります。

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

Salesforce、テーブルビュー、データソースからエントリを削除する

リスト画面で残っている作業は、削除要求をサーバに送信することだけです。

REST 要求を作成するとき、その ID を deleteRequests 辞書でオブジェクトのキーとして使用します。後で、同じ ID を再利用して、削除された項目の情報を取得できます。

ここでは、iOS によって重要な規則が適用されています。テーブルビューから行を削除する場合は、同じコードブロック内でデータソースから対応するエントリを削除する必要があるというものです。したがって、このメソッド内で行とデータソースエントリの両方を削除します。

  1. 前の if ブロックの最後に続けて、RestClient の共有インスタンスを参照する定数を提供します。
    let restClient = RestClient.shared
  2. Salesforce オブジェクトを削除するための RestRequest オブジェクトを作成します。要求を作成するには、「Contact」オブジェクト種別と deletedId 値を使用して、restApi.requestForDelete メソッドをコールします。この 2 つの値で、削除する Salesforce レコードを正確に示します。
    let deleteRequest =
        restClient.requestForDelete(withObjectType: "Contact", 
            objectId: deletedId, apiVersion: nil)
  3. Salesforce に要求を送信するには、RestClient send(request:_:) 関数をコールします。トレイリングクロージャが 2 つ目の引数の代わりに使用され、成功または失敗の応答種別を処理できます。この非同期クロージャで省略可能項目の必要性を排除するには、guard ステートメントを使用して、self が依然としてメモリ内にあることを保証します。
    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. .Success ケースでは、アプリケーションの取引先責任者リストを更新します。最初に、メインスレッドで実行するブロックを追加します。このようなブロックは、非同期で実行する UI 更新で使用します。
    case .success(_):
        DispatchQueue.main.async {
    
        }
    }
  5. DispatchQueue.main.async ブロックで、deleteRequests 辞書に deletedItemInfo 配列をキャッシュします。RestRequest オブジェクトの ID (request.hashValue) をキーとして使用します。
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                    
        }
    }
  6. 表示を更新するには、dataRows オブジェクトから行を削除し、テーブルビューを再度読み込みます。この場合は response を使用しないため、let response_ に置換します。
    case .success(_):
        DispatchQueue.main.async {
            strongSelf.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
            strongSelf.dataRows.remove(at: row)
            strongSelf.tableView.reloadData()
        }
  7. .Failure ケースでは、エラーメッセージを記録します。後ほどこのブロックに戻ってエラー処理を強化します。
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
    
これで完了です。ここまでの tableView(_:editingStyleForRowAt:) メソッドと tableView(_:commit:forRowAt:) メソッドを示します。
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)
            }
        }
    }
}

最後の仕上げ

★新しいアプリケーションにユーザ操作を追加しました。サーバ上のレコードを削除するボタンのみですが、この最小限のテンプレートにとっては大きな変化です。では、失敗処理の問題に取り組みましょう。普遍的に存在する失敗ではありませんが、削除要求が機能しない場合は、どのように復元すればよいでしょうか。

削除が失敗したことを知るまでに、dataRows 配列とテーブルビューの両方からすでにレコードが削除されています。明らかに、後戻りしてそれらの項目を元に戻す必要がありますが、どうすればよいのでしょうか? 幸い、十分に準備ができています。deleteRequests 辞書にすべての削除 REST 要求オブジェクトをキャッシュしてあります。賢明でしたね。各 REST エラーは関連する RestRequest オブジェクトを返すため、削除されたコンテンツを deleteRequests 辞書内で簡単に見つけることができます。

reinstateDeleteRowWithRequest(_:) メソッドを定義する

  1. RootViewController クラス定義の最後に、reinstateDeleteRowWithRequest(_: RestRequest) メソッドを空の実装ブロックと共に追加します。
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
    
    }
  2. このメソッドは UI 要素を変更するため、最初のブロックで、すべてのコールをメインスレッドで実行するようにします。
    func reinstateDeletedRowWithRequest(_ request: RestRequest) {
        DispatchQueue.main.async {
            
        }
    }
  3. DispatchQueue.main.async: ブロックでは、要求引数の ID を使用して、削除されたデータを deleteRequests 辞書内で検索します。このメソッドの残りは、この値が nil 以外の場合にのみ適用されます。
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
    
        }
    }
  4. 削除した行の値の取得に成功した場合、データオブジェクトを dataRows 辞書のインデックス 0 の位置に再挿入します。
    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. 削除された取引先責任者名を元に戻すには、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. 取引先責任者の復元が完了したので、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)
        }
    }

    次に、完成した reinstateDeleteRowWithRequest(_:) メソッドを示します。

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

REST 応答エラーを処理する

REST 応答で成功または失敗を示すことができます。この例では、成功は行が正常に削除されたことを意味します。失敗は、行を削除できなかったことや、接続が切断されたまたはタイムアウトしたことを示します。この例では、ネットワークがエラーで応答したことを意味します。

新しい reinstateDeleteRowWithRequest(_:) メソッドを使用できるようになりました。REST 応答でエラーが示された場合に、このメソッドをコールします。この Swift アプリケーションでは、RestClient.send() メソッドの onFailure クロージャを使用して、REST 応答エラーを処理します。このブロックで 3 つの Objective-C RestClientDelegate エラーハンドラを置き換えます。

このアプリケーションでは、エラーの種類に関係なく reinstateDeleteRowWithRequest(_:) をコールします。また、アラートボックスでユーザにエラーメッセージを表示します。アラートボックスのコードを再利用できるように、コードを独自のメソッド本文に挿入しましょう。

  1. RootViewController クラス本文で、showErrorAlert という名前の新しい非公開メソッドを作成します。このメソッドは、catch ブロックに渡す RestClientError オブジェクトという 1 つの引数を取ります。
    private func showErrorAlert(_ error: RestClientError) {
    
    }
  2. DispatchQueue.main.async ブロックで、アラートボックスを作成して表示します。アラートボックスの文字列には、error._userInfo 配列の message メンバーを使用します。このコードは少し乱雑に見えます。userInfo がアプリケーションで定義され、省略可能であるためです。これを使用する前に、期待される構造を持つ userInfo 配列が、キャッチされたエラーに含まれるようにします。
    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. tableView(_:commit:forRowAt:) メソッドで、RestClient send() メソッドの .failure ケースの新しいメソッドをコールします。
    case .failure(let error):
        os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
        strongSelf.reinstateDeletedRowWithRequest(deleteRequest)
        strongSelf.showErrorAlert(error as RestClientError)
    }
    次に、完成した tableView(_:commit:forRowAt:) メソッドを示します。
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)
            }
        }
    }
}

実際に試してみましょう。

これで終わりです。iOS シミュレータでコードをビルドして実行する準備ができました。Developer Edition データベース内のデフォルトの取引先責任者を削除しようとすると、エラー応答が返されます。これらのエラーは、Developer Edition 組織にあらかじめパッケージされた各取引先責任者が他のレコードの親であるために発生します。テストの準備をするには、Developer Edition 組織にログインし、他のレコードを所有していないテスト取引先責任者を 1 件以上作成します。