非同期処理と出会った話 ー後編ー
前回では、Alamofireを使用して通信には成功したけど、TableViewに反映しようとするとアプリがクラッシュするということ書きました。
今回もコードの一部分を載せていきます。
- RequestOkashi.swift
class RequestOkashi{ var okashiList : [(name:String, maker:String, link:URL, image:URL)] = [] // 引数keywordはUISearchBarに入力する検索したいキーワード func searchOkashi(keyword : String) -> [(name:String, maker:String, link:URL, image:URL)] { // 全角文字をエンコードやリクエストURLの生成は省略 AF.request(requestURL, method: .get, encoding: JSONEncoding.default).response { response in switch response.result { case .success( _): guard let data = response.data else {return} let decoder = JSONDecoder() guard let okashi = try? decoder.decode(ResultJson.self, from: data) else { return } guard let items = okashi.item else { return } self.okashiList.removeAll() for item in items { guard let name:String = item.name, let maker:String = item.maker, let link:URL = item.url, let image = item.image else { return } let okashiTuple = (name, maker, link, image) self.okashiList.append(okashiTuple) } case .failure(let error): print(error) } } return self.okashiList } }
前回と違うところは、searchOkashi()メソッドに戻り値を持たせたところです。ViewControllerで戻り値を受け取ってTableViewに反映しようという考えです。ViewControllerは前回とたいして変わっていないのでここでは書かないことにしました。。。
これで実行してみます。
なんと、前回と同じくクラッシュしてしまいました。「もしかしたら、AF.request()の中と外でokashiListの中身が違うのでは?」と思い、print文を書いて確かめてみました。すると、AF.request()の中ではokashiListにデータが入っているけど、外では中身が空っぽ。すなわち初期値の状態で表示させたのです。
うーん、やっぱり。って感じです。。
色々調べてみると、
{response in ...}の部分は、完了ハンドラとしてresponseメソッドに渡されますが、その完了ハンドラが実行されるのは通信が完了した後です。
スタック・オーバーフローにそれっぽことが書いてあった!!
呼び出して戻ってきた値が空だったので、return self.okashiList
が実行されるのは、通信が完了する前だということが分かりました。
どうするのかというと、自分で作ったsearchOkashi()メソッドを完了ハンドラのパターンに当てはめます。
つまり、、、
- 受け取りたい型の引数のクロージャを引数として追加
- Alamofireの完了ハンドラのなかで自前の完了ハンドラを呼ぶ
これらのことをやっていきます。
- RequestOkashi.swift
class RequestOkashi{ var okashiList : [(name:String, maker:String, link:URL, image:URL)] = [] // 引数keywordはUISearchBarに入力する検索したいキーワード func searchOkashi(keyword : String , completion: @escaping ([(name:String, maker:String, link:URL, image:URL)]) -> Void){ // 全角文字をエンコードする guard let keywordEncode = keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return } // リクエストURL生成 guard let requestURL = URL(string: "https://sysbird.jp/toriko/api/?apikey=guest&format=json&keyword=\(keywordEncode)&max=10&order=r") else { return } print(requestURL) AF.request(requestURL, method: .get, encoding: JSONEncoding.default).response { response in // { response in ...} の部分は完了ハンドラとして渡される // その完了ハンドラが渡されるのは通信完了後 // しかし、return okashiListが実行されるのは通信完了前 // だから、結果が[]やnillになる switch response.result { case .success( _): guard let data = response.data else {return} let decoder = JSONDecoder() guard let okashi = try? decoder.decode(ResultJson.self, from: data) else { return } guard let items = okashi.item else { return } self.okashiList.removeAll() for item in items { guard let name:String = item.name, let maker:String = item.maker, let link:URL = item.url, let image = item.image else { return } let okashiTuple = (name, maker, link, image) self.okashiList.append(okashiTuple) } //元の非同期処理の完了ハンドラの中で自前の完了ハンドラを呼び出す completion(self.okashiList) case .failure(let error): print(error) } } } }
- ViewController.swift
class ViewController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate{ @IBOutlet weak var searchText: UISearchBar! @IBOutlet weak var tableView: UITableView! var request = RequestOkashi() var okashiList : [(name:String, maker:String, link:URL, image:URL)] = [] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. searchText.delegate = self searchText.placeholder = "お菓子の名前を入力してください" tableView.dataSource = self tableView.delegate = self } // 検索ボタンクリック時に呼ばれるdelegateメソッド func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { // キーボードを閉じる view.endEditing(true) guard let searchWord = searchBar.text else { return } print(searchWord) // 非同期処理の呼び出し request.searchOkashi(keyword: searchWord, completion: { okashiList in self.okashiList = okashiList print(self.okashiList) self.tableView.reloadData() }) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { print(self.okashiList.count) return okashiList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // お菓子の名前をcellに表示 let cell = tableView.dequeueReusableCell(withIdentifier: "okashiCell", for: indexPath) cell.textLabel?.text = okashiList[indexPath.row].name // お菓子の画像をcellに表示 if let imageData = try? Data(contentsOf: okashiList[indexPath.row].image){ cell.imageView?.image = UIImage(data: imageData) } return cell } }
やっとできた!! これで動いたよ!!
非同期処理を上手く書けると、複数のAPIを叩いて、全ての通信系の処理が終わってから次の処理に移るということができそうです。
その辺を上手くやってくれるのがRxSwiftみたいですけど。。。
RxSwiftは現在、勉強中です。(結構難しい。。。)
最後にもう一度、完成版を載せておきます。
github.com
以下のサイトを参考にしました。
Alamofireを使ったメソッドで返り値を取得する - Qiita
swift - Alamofireでのリクエスト結果を返り値にしたい - スタック・オーバーフロー