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

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

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

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

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

Q&A

解決済

1回答

1208閲覧

UITableViewCellのインスタンス代入の仕方

KYSFY

総合スコア3

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

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

0グッド

0クリップ

投稿2021/05/08 16:43

TableViewCell⇨UIViewControllerに画面遷移。

遷移先に「お気に入りボタン(★)」を配置。
タップで黄色⇄グレーに変化。

この内容をTableViewCellにも配置してる「お気に入りボタン(★)」に反映させたい。

Swift
Xcode12.3

ビルドはできますが、画面遷移時に下記のエラーメッセージになります。

Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

試したこと

インスタンスを代入するコードを入力しましたが、うまく行きません。
(下記コード内に記載してます)

全コード記載します。

モデル

import Foundation struct Animal { private(set) public var nameJP : String private(set) public var imageName : String private(set) public var star : String init(nameJP: String, imageName: String,star: String) { self.nameJP = nameJP self.imageName = imageName self.star = star } }
import UIKit class ViewController: UIViewController { @IBOutlet weak var kensaku: UIButton!    //タップでTableViewに画面遷移 override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } }
import UIKit @available(iOS 10.0, *) class ListViewController: UIViewController,UITableViewDelegate, UITableViewDataSource{ @IBOutlet weak var myTableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! var animals: [Animal] = [] var dupList: [Animal] = [] var detailViewController : DetailViewController! var animalTableViewCell : AnimalTableViewCell! override func viewDidLoad() { super.viewDidLoad() myTableView.dataSource = self myTableView.delegate = self searchBar.delegate = self loadData() animalTableViewCell.star.isHidden = true    **//ViewControllerからの画面遷移時 ここでエラーメッセージ** } func loadData() { animals.append(Animal(nameJP: "犬", imageName: "dog", star: "staryellow")) animals.append(Animal(nameJP: "猫", imageName: "cat", star: "staryellow")) animals.append(Animal(nameJP: "猿", imageName: "monkey", star: "staryellow")) animals.append(Animal(nameJP: "鳥", imageName: "bird", star: "staryellow")) //以下多数続く self.dupList = self.animals } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.dupList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = myTableView.dequeueReusableCell(withIdentifier: "AnimalTableViewCell", for: indexPath) as? AnimalTableViewCell else { fatalError("Dequeue failed: AnimalTableViewCell.") } cell.animalNameJPLabel.text = self.dupList[indexPath.row].nameJP // cell.animalNameENLabel.text = self.dupList[indexPath.row].nameEN cell.animalImageView.image = UIImage(named: self.dupList[indexPath.row].imageName) cell.star.image = UIImage(named: self.dupList[indexPath.row].star) return cell } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetailSegue" { if let indexPath = myTableView.indexPathForSelectedRow { guard let destination = segue.destination as? DetailViewController else { fatalError("Failed to prepare DetailViewController.") detailViewController.animalTableViewCell = self.animalTableViewCell detailViewController.listViewController = self } destination.animal = dupList[indexPath.row] } } } override func viewWillAppear(_ animated: Bool) { if let indexPath = myTableView.indexPathForSelectedRow{ myTableView.deselectRow(at: indexPath, animated: true) } } } //searchBar の機能を拡張 @available(iOS 10.0, *) extension ListViewController: UISearchBarDelegate { func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { searchBar.text = "" searchBar.endEditing(true) dupList = animals } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { print("search = (searchText)") dupList = searchText.isEmpty ? animals : animals.filter({ (Animal) -> Bool in return Animal.nameJP.range(of: searchText, options: .caseInsensitive, range: nil, locale: nil) != nil }) myTableView.reloadData() } }
import UIKit @available(iOS 10.0, *) class DetailViewController: UIViewController { var animalTableViewCell : AnimalTableViewCell! var listViewController : ListViewController! var animal: Animal! @IBOutlet weak var animalImageView: UIImageView! @IBOutlet weak var animalNameJPLabel: UILabel! @IBOutlet weak var okiniYellow: UIButton! @IBOutlet weak var okiniGray: UIButton! override func viewDidLoad() { super.viewDidLoad() okiniYellow.isHidden = true animalImageView.image = UIImage(named: animal.imageName) animalNameJPLabel.text = animal.nameJP } //お気に入り★マークをタップして黄色とグレーに切り替える。      //この内容をTableViewselの★マークにも反映させたい。 @IBAction func tapOkiniGray(_ sender: Any) { okiniGray.isHidden = true okiniYellow.isHidden = false animalTableViewCell.star.isHidden = false } @IBAction func tapOkiniYellow(_ sender: Any) { okiniGray.isHidden = false okiniYellow.isHidden = true animalTableViewCell.star.isHidden = true } }
import UIKit @available(iOS 10.0, *) class AnimalTableViewCell: UITableViewCell { var detailViewController : DetailViewController! var listViewController : ListViewController! @IBOutlet weak var animalNameJPLabel: UILabel! @IBOutlet weak var animalImageView: UIImageView! @IBOutlet weak var star: UIImageView! override func awakeFromNib() { super.awakeFromNib() listViewController.animalTableViewCell = self detailViewController.animalTableViewCell = self      //これを入れてみましたが、うまく行きません。 } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } }

初めてTableViewを使ってますが、理解仕切れてないので、助けて頂けますでしょうか。

また、この内容でお気に入り★マークの内容がちゃんと「各セル別に内容反映」されるのか、
「セル別にユーザーデフォルツ保存」できるのか、合わせてご教授お願いいたします。

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

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

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

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

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

hoshi-takanori

2021/05/08 18:19

どの画面に遷移した時に、どこでエラーになってるかを書いて欲しいのですが、とりあえずどこかの @IBOutlet の接続を忘れてるのでは。
KYSFY

2021/05/08 18:28

「class ListViewController」のコード内に記載してます。 「class ViewController」から「class ListViewController」に画面遷移した際、 「animalTableViewCell.star.isHidden = true」の部分でエラーになります。 宜しくお願いいたします。
hoshi-takanori

2021/05/08 19:16

ListViewController が animalTableViewCell というプロパティを持ってますが、そもそもそれがおかしいですね。UITableView の使い方をあまりよく理解されてないのでは…。
hoshi-takanori

2021/05/08 19:51

アプリの仕様を確認したいのですが、動物図鑑的なもので、動物のリストは固定 (ユーザーが動物を増やしたり減らしたりはしない) で、 UserDefaults に保存したいのは各動物がお気に入りかどうかの情報ってことでしょうか?
KYSFY

2021/05/08 20:13

詳細の確認、ありがとうございます!動物は質問用に内容替えたものですが、仰る内容に近いものです。 リストは固定。ユーザーは増やせません。私の方で定期的に追加してバージョンUPはあり。 ユーザーがお気に入り⭐️チェックをしたものを、cellの部分でも⭐️表示して、cellとcellからの遷移先の両方で保存したい。という内容になります。宜しくお願いいたします。
tomato879241

2021/05/08 23:19

1. loadData()の中のself.dupList = self.animalsは何のためでしょうか? 2. どうしてviewDidLoadの中でloadData()を呼ぶのでしょうか?viewDidLoadの役割を知っていますか? 3. loadData()の最後にmyTableViewをreloadDataしていないのでは?
hoshi-takanori

2021/05/08 23:53 編集

代わりにお答えします。 1. dupList は検索 (絞り込み) 結果です。最初に animals を代入するのは、最初は全てを表示するためです。(変数名がいけてないというご指摘であれば、その通りだと思います。) 2. viewDidLoad の本来の目的は xib や storyboard から読み込んだ直後にビューを設定することです。以前 (iOS 5 あたりまで) は画面遷移に伴ってメモリの節約のためにビューを unload して load し直すということがありましたが、現在ではそれは廃止されたので、事実上 viewDidLoad はビューコントローラーの初期化のためにも使われています。(本来は init でやるべきでしょうけど、UIViewController の init は複数あるため、あまり定義したくないという理由もあります。) 3. loadData() を呼ぶのは viewDidLoad だけなので、reloadData の必要はありません。
guest

回答1

0

ベストアンサー

まず、不要なプロパティがあるので、これらを削除して、これらを使った処理も消しましょう。

  • ListViewController の detailViewController と animalTableViewCell
  • DetailViewController の animalTableViewCell と listViewController
  • AnimalTableViewCell の detailViewController と listViewController

これらはお互いに参照しあっていますが、特に ListViewController に関しては、一つの画面に複数の項目を表示しているので、detailViewController や animalTableViewCell を一つだけプロパティに持つ意味はありません。また、テーブルのセルは UITableView が管理してますし、ビューコントローラー同士も画面遷移に伴って OS が管理するので、あまりお互いを知る必要はないです。(たまに呼び出し先が呼び出し元のビューコントローラーのメソッドを呼びたい場合もありますが、そういう場合は弱参照の delegate にします。)

問題は DetailViewController のお気に入りボタンの処理ですが、表示部品である animalTableViewCell の star をいじるのではなく、まずデータを更新して、そのデータに基づいて表示を更新する、という形にする必要があります。

そのためにはまずデータの見直しが必要です。

  • Animal は class にしましょう。構造体だと、DetailViewController に渡した時に内容がコピーされるので、DetailViewController でデータを更新してもそれはコピーに対する操作になり、元のデータには反映されません。
  • nameJP と imageName の値は変更しないので、let にすると良いでしょう。
  • star を文字列にしていますが、持つべき情報はお気に入りかどうかなので、Bool 型が良いでしょう。
    また、この値はお気に入りボタンが押された時に変更できるように var にします。
  • Animal のイニシャライザは nameJP と imageName だけを指定すれば良いと思います。
    star の初期値は UserDefaults から読み込むので、最初は false にしておきましょう。

swift

1class Animal { 2 let nameJP: String 3 let imageName: String 4 var star = false 5 6 init(nameJP: String, imageName: String) { 7 self.nameJP = nameJP 8 self.imageName = imageName 9 } 10}

ListViewController の loadData メソッドでは、star: "staryellow" を削除します。

swift

1 func loadData() { 2 animals.append(Animal(nameJP: "犬", imageName: "dog")) 3 // 以下略

ListViewController の cellForRowAt メソッドですが、セルのお気に入り★マークをお気に入りのものだけに表示するのであれば、Animal の star の値に応じて cell.star の表示・非表示を切り替えると良いでしょう。

swift

1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 2 // 略 3 4 cell.star.isHidden = !self.dupList[indexPath.row].star 5 6 return cell 7 }

ここで DetailViewController に移って、viewDidLoad メソッドでは animal.star の値に応じて okiniGray または okiniYellow のどちらかを非表示にします。

swift

1 override func viewDidLoad() { 2 // 略 3 4 if animal.star { 5 okiniGray.isHidden = true 6 } else { 7 okiniYellow.isHidden = true 8 } 9 }

また、tapOkiniGray および tapOkiniYellow では、animalTableViewCell.star.isHidden ではなく、animal.star の値を true または false にします。

swift

1 @IBAction func tapOkiniGray(_ sender: Any) { 2 // 略 3 animal.star = true 4 } 5 6 @IBAction func tapOkiniYellow(_ sender: Any) { 7 // 略 8 animal.star = false 9 }

最後に ListViewController に戻って、viewWillAppear メソッドは ListViewController が最初に表示される時や、DetailViewController から戻ってきた時に呼ばれますので、ここで表示を更新すると良いでしょう。(また、super の呼び出しをお忘れなく。)

swift

1 override func viewWillAppear(_ animated: Bool) { 2 super.viewWillAppear(animated) 3 4 if let indexPath = myTableView.indexPathForSelectedRow{ 5 myTableView.deselectRow(at: indexPath, animated: true) 6 } 7 myTableView.reloadData() 8 }

とりあえずこれでお気に入りの処理はご希望通りになると思います。UserDefaults への保存は、ここまでの修正でちゃんと動くことを確認できてから追記しますので、コメント欄で結果をお知らせください。


UserDefaults への保存ですが、まずどんな値をどのような形で保存するかを考える必要があります。

UserDefaults.standard.set(okiniGray.isHidden, forKey: "saveGray")
UserDefaults.standard.set(okiniYellow.isHidden, forKey: "saveYellow")
UserDefaults.standard.set(animal.star, forKey: "saveCellStar")

3 つも必要でしょうか? 保存したいのはお気に入りかどうかなので animal.star を保存すれば充分で、okiniGray や okiniYellow の値は animal.star の値が決まれば自動的に決まります。

UserDefaults.standard.set(animal.star, forKey: "saveCellStar")

また、これだと別の問題があって、どの動物をお気に入りにしたかの区別がつかないですよね。

というわけで、保存したいのは「各動物をお気に入りにしたかどうか」になります。もっと言うと、「犬をお気に入りにしたか」「猫をお気に入りにしたか」「猿をお気に入りにしたか」「鳥をお気に入りにしたか」…、をそれぞれ保存する必要があるということです。

方法はいくつか考えられますが、直感的なのは辞書を使うことではないでしょうか。

swift

1let stars: [String: Bool] = [ 2 "犬": true, 3 "猫": false, 4 "猿": true, 5 "鳥": false, 6]

もう一つの方法として、お気に入りの動物の配列を使うこともできます。この場合、配列に含まれないものはお気に入りではない、ということになります。

swift

1let stars: [String] = ["犬", "猿"]

配列の方が簡単なので、配列を使うことにしましょう。まず保存のメソッドを ListViewController に書きます。一時的にお気に入りの動物の名前だけを集めた配列を作って、それを保存します。
(デバッグビルドの場合に synchronize してますが、これは Xcode からプログラムを停止させると UserDefaults が保存されないことがあるのを防ぐためです。)

swift

1 func saveStars() { 2 var stars = [String]() 3 for animal in animals { 4 if animal.star { 5 stars.append(animal.nameJP) 6 } 7 } 8 print("saveStars: (stars)") // デバッグ用、確認したら消す 9 10 UserDefaults.standard.set(stars, forKey: "stars") 11 #if DEBUG 12 UserDefaults.standard.synchronize() 13 #endif 14 }

次に読み込みです。これも ListViewController に書きます。お気に入りの配列を読み込んで、それに含まれる動物の star を true にします。(含まれない動物の star は初期値 false なので、そのままで良い。)

swift

1 func loadStars() { 2 guard let stars = UserDefaults.standard.array(forKey: "stars") as? [String] else { 3 return 4 } 5 print("loadStars: (stars)") // デバッグ用、確認したら消す 6 7 for animal in animals { 8 if stars.contains(animal.nameJP) { 9 animal.star = true 10 } 11 } 12 }

これらを呼び出す方法ですが、loadStars は loadData で animals の配列を作り終わった後に一回だけ呼び出せば良いでしょう。

問題は saveStars ですが、お気に入りボタンを押すたびに保存したいので、DetailViewController から ListViewController のメソッドを呼び出すことになります。このためには DetailViewController が ListViewController の参照を持つ必要がありますが、循環参照を防ぐために weak (弱参照) にする必要があります。(本来はプロトコルを定義して delegate にしたいところですが、今はまだいいでしょう。)

diff

1 class DetailViewController: UIViewController { 2 3+ weak var listViewController: ListViewController? 4 5 // 略

次に、画面遷移の際に listViewController を設定します。

diff

1 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 2 if segue.identifier == "showDetailSegue" { 3 if let indexPath = myTableView.indexPathForSelectedRow { 4 let destination = segue.destination as! DetailViewController 5+ destination.listViewController = self 6 destination.animal = dupList[indexPath.row] 7 } 8 } 9 }

最後に、お気に入りボタンを押すたびに saveStars を呼び出せば完成です。

diff

1 @IBAction func tapOkiniGray(_ sender: Any) { 2 // 略 3 animal.star = true 4+ listViewController?.saveStars() 5 } 6 7 @IBAction func tapOkiniYellow(_ sender: Any) { 8 // 略 9 animal.star = false 10+ listViewController?.saveStars() 11 }

投稿2021/05/08 23:31

編集2021/05/09 10:39
hoshi-takanori

総合スコア7901

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

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

KYSFY

2021/05/09 00:40

ありがとうございます!ちゃんと動きました!
hoshi-takanori

2021/05/09 01:10

動いてよかったです。 UserDefaults への保存ですが、まず何をどういう形で保存するかを考える必要があります。 今日の夕方までには回答を追記する予定ですが、その前に少しだけ考えてもらってもいいですか? 考えたことはこのコメント欄にお書きください。
KYSFY

2021/05/09 06:07

DetailViewController @IBAction func tapOkiniGrayのところで、 okiniGray.isHiddenの値を保存。(Yellowも同様) animal.starのBool値を保存。 保存したforKeyの呼び出し。 animal.starのBool値の記載方法と、呼び出しはどこが適切なのかがよく分かってないので、 試してみます。
KYSFY

2021/05/09 07:11

以下の様にsetしました。 DetailViewController @IBAction func tapOkiniGray(_ sender: Any) { animal.star = true okiniGray.isHidden = true okiniYellow.isHidden = false saveStar() } @IBAction func tapOkiniYellow(_ sender: Any) { animal.star = false okiniYellow.isHidden = true okiniGray.isHidden = false saveStar() } func saveStar(){ UserDefaults.standard.set(okiniGray.isHidden, forKey: "saveGray") UserDefaults.standard.set(okiniYellow.isHidden, forKey: "saveYellow") UserDefaults.standard.set(animal.star, forKey: "saveCellStar") } 呼び出しのコード UserDefaults.standard.set(okiniGray.isHidden, forKey: "saveGray") UserDefaults.standard.set(okiniYellow.isHidden, forKey: "saveYellow") UserDefaults.standard.set(animal.star, forKey: "saveCellStar") これをListViewControllerとViewControllerの、viewDidLoadとviewDidAppear内で試してみたのですが、いずれも呼び出されずでした。いずれもエラーは出ません。 ちゃんとsetされてないのか、呼び出す場所が間違ってるのか。。?
KYSFY

2021/05/09 15:08

ありがとうございます!これから仕事で出掛けてしまう為、明日の昼過ぎに作業する予定です。 終わり次第、別途結果の報告させて頂きます。色々ゆっくり勉強させて頂きます!
KYSFY

2021/05/10 04:54

hoshi-takanoriさん 問題なくやりたかった事が実装できました。ご丁寧な説明も記載して頂き、大変分かり易かったです。本当にありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問