質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
MVP

MVP(Minimum Viable Product)とは、「必要最低限の機能を兼ね備えた製品」を指します。企画書などを完成させる前に、とりあえず製品を形にする方法です。プロトタイプなどで一旦アウトプットさせることにより、無駄なコストや時間を削減できます。

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

820閲覧

【Swift】UIRefreshControl使用時のエラー

duck015

総合スコア29

MVP

MVP(Minimum Viable Product)とは、「必要最低限の機能を兼ね備えた製品」を指します。企画書などを完成させる前に、とりあえず製品を形にする方法です。プロトタイプなどで一旦アウトプットさせることにより、無駄なコストや時間を削減できます。

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2020/02/06 06:55

編集2020/02/06 07:05

前提・実現したいこと

teratailのAPIを叩き、質問一覧をTableViewに表示させたいです(Alamofire SwiftyJSONを使用)。
MVPの練習をしたく、アーキテクチャはMVPを採用しています。

QuestionModel内でAPIを叩き、配列questionsに追加しています。
そして、処理結果をQuestionListViewPresenterへ渡し、NewArrivalQuestionListViewControllerでTableViewに表示させるといった流れです。

発生している問題・エラーメッセージ

データ自体はTableViewに表示されて問題ないのですが、TableViewを下に引っ張った時にUIRefreshControlでデータ更新をしたいです。
現状だと下に引っ張ったときに下記エラーが出ています。

UIRefreshControlのインスタンスには関数updateQuestions()addTargetで指定しています。
このupdateQuestions()はNewArrivalQuestionListViewControllerのviewDidLoadでも使用しています。
viewDidLoadの時には問題ないのに、なぜ下に引っ張った時にエラーになるのか疑問です。

QuestionListViewPresenterのfunc entity()でエラーが発生していますが、データを格納している配列questionsを見るとデータが入っていないため、エラーになっているようです。
そのため、恐らくAlamofireの非同期通信が影響しているのではと思っています。

しかし、QuestionModel内の関数fetchQuestions()ではNotificationCenterを使用し、QuestionListViewPresenterへ通知を行っているため、その影響は無いはずです(実際にviewDidLoadで表示できているため)。

色々と考えてみましたが、原因が分からずお手上げの状況のため、ご教示いただけませんでしょうか?
補足ですが、fetchQuestions()の中のprint("通知")は下に引っ張った時に出力されませんでした。
(つまり通知する前にエラーになっている?)

エラー内容:Fatal error: Index out of range QuestionListViewPresenterのこの箇所で。 エラー発生箇所 : func entity(at indexPath: IndexPath) -> QuestionEntity { return questionModel.questions[indexPath.row] }

該当のソースコード

QuestionEntity

1struct QuestionEntity { 2 var id: Int 3 var title: String 4 var tags: [String] 5 var displayName: String 6 var photo: String 7 var created: String 8 var isAccepted: Bool 9 10 init(id: Int, title: String, tags: [String], displayName: String, photo: String, created: String, isAccepted: Bool) { 11 self.id = id 12 self.title = title 13 self.tags = tags 14 self.displayName = displayName 15 self.photo = photo 16 self.created = created 17 self.isAccepted = isAccepted 18 } 19}

QuestionModel

1class QuestionModel { 2 3 var questions: [QuestionEntity] = [] 4 var notificationName: Notification.Name { 5 return Notification.Name(rawValue: "questions") 6 } 7 8 func fetchQuestions() { 9 print("fetch") 10 Alamofire.request("https://teratail.com/api/v1/questions").responseJSON { response in 11 guard let object = response.result.value else { return } 12 let json = JSON(object) 13 json["questions"].forEach { (_, json) in 14 let id = json["id"].intValue 15 let title = json["title"].stringValue 16 let tags = json["tags"].arrayObject 17 let displayName = json["user"]["display_name"].stringValue 18 let photo = json["user"]["photo"].stringValue 19 let created = json["created"].stringValue 20 let isAccepted = json["isAccepted"].boolValue 21 self.questions.append(QuestionEntity(id: id, title: title, tags: tags as! [String], displayName: displayName, photo: photo, created: created, isAccepted: isAccepted)) 22 } 23 self.notify() 24 print("通知") 25 } 26 } 27 28 func refreshQuestions() { 29 questions = [] 30 print("refresh") 31 } 32 33 func addObserver(_ observer: Any, selector: Selector) { 34 NotificationCenter.default.addObserver(observer, selector: selector, name: notificationName, object: nil) 35 } 36 37 func removeObserver(_ observer: Any) { 38 NotificationCenter.default.removeObserver(observer) 39 } 40 41 func notify() { 42 NotificationCenter.default.post(name: notificationName, object: nil) 43 } 44} 45

QuestionListViewPresenter

1class QuestionListViewPresenter { 2 3 private weak var view: ListViewInterface! 4 5 var questionModel: QuestionModel! 6 7 var numberOfQuestions: Int { 8 return questionModel?.questions.count ?? 10 9 } 10 11 init(view: ListViewInterface) { 12 self.view = view 13 self.questionModel = QuestionModel() 14 self.questionModel.addObserver(self, selector: #selector(self.updated)) 15 } 16// 17// deinit { 18// questionModel.removeObserver(self) 19// } 20// 21 @objc func updateQuestions() { 22 questionModel.refreshQuestions() 23 questionModel.fetchQuestions() 24 print("presenter") 25 } 26 27 func entity(at indexPath: IndexPath) -> QuestionEntity { 28 return questionModel.questions[indexPath.row] //Fatal error: Index out of range 29 } 30 31 @objc func updated() { 32 view?.reloadData() 33 } 34} 35

NewArrivalQuestionListViewController

1protocol ListViewInterface: class { 2 func reloadData() 3} 4 5class NewArrivalQuestionListViewController: UIViewController, ListViewInterface { 6 7 @IBOutlet weak var newArrivalQuestionListTableView: UITableView! 8 9 var presenter: QuestionListViewPresenter! 10 11 var itemInfo: IndicatorInfo = "新着" 12 13 override func viewDidLoad() { 14 super.viewDidLoad() 15 initializePresenter() 16 initializeTableView() 17 newArrivalQuestionListTableView.refreshControl = UIRefreshControl() 18 newArrivalQuestionListTableView.refreshControl?.addTarget(self, action: #selector(self.updateQuestions), for: .valueChanged) 19 updateQuestions() 20 } 21 22 func initializeTableView() { 23 newArrivalQuestionListTableView.delegate = self 24 newArrivalQuestionListTableView.dataSource = self 25 newArrivalQuestionListTableView.register(UINib(nibName: "QuestionListTableViewCell", bundle: nil), forCellReuseIdentifier: "QuestionListTableViewCell") 26 } 27 28 func initializePresenter() { 29 presenter = QuestionListViewPresenter(view: self) 30 } 31 32 func reloadData() { 33 newArrivalQuestionListTableView.refreshControl?.endRefreshing() 34 newArrivalQuestionListTableView.reloadData() 35 print("リロード") 36 } 37 38 @objc func updateQuestions() { 39 presenter.updateQuestions() 40 newArrivalQuestionListTableView.refreshControl?.beginRefreshing() 41 } 42} 43 44extension NewArrivalQuestionListViewController: UITableViewDelegate, UITableViewDataSource { 45 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 return presenter.numberOfQuestions 47 } 48 49 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 let cell = newArrivalQuestionListTableView.dequeueReusableCell(withIdentifier: "QuestionListTableViewCell", for: indexPath) as! QuestionListTableViewCell 51 cell.setQuestion(entity: presenter.entity(at: indexPath)) 52 return cell 53 } 54 55 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 56 newArrivalQuestionListTableView.deselectRow(at: indexPath, animated: true) 57 } 58}

参考記事

【Swift】MVCから脱却したいのでMVPの勉強をした

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

QuestionListViewPresenterのupdateQuestionsでrefreshQuestionsとfetchQuestionsを呼び出してますが、これらはそれぞれ
・refreshQuestions: questionsを空にする
・fetchQuestions: APIを叩き、結果が返ったらquestionsに値を入れてnotifyを呼び、表示を更新する
という処理をしています。ここで、「結果が返ったら〜する」の部分は非同期で実行されるので、updateQuestions自体はnotify(reloadData)が呼ばれる前に終了します。このため、引っ張った時はquestionsが変更されたのにreloadDataされてない状態になり、TableViewが存在すると思い込んでいるデータを参照しようとしてエラーになっています。なお、初回はもともとquestionsが空なので問題ありません。

修正方法としては、
・refreshQuestionsでquestionsを空にしたらnotifyを呼ぶ、または
・updateQuestionsではrefreshQuestionsせずに、APIの結果が返ってきた時にquestionsをいったん空にしてから結果を格納する
などでしょうか。

投稿2020/02/08 08:49

hoshi-takanori

総合スコア7895

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

duck015

2020/02/09 13:27

hiroshi-takanori様 ご回答ありがとうございます。なるほど、非同期通信の影響だったのですね...。 頂いたアドバイスの通り実装したところ、問題なく動作しました。 ありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問