🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Swift

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

Q&A

解決済

1回答

1104閲覧

UserDefaults.standardに保存した構造体をLabelに適応するにはどうすれば良いのでしょうか?

zukaibito

総合スコア1

Swift

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

0グッド

0クリップ

投稿2021/02/13 13:38

編集2021/02/16 15:22

前提・実現したいこと

NextViewControllerで構造体をUserDefaults.standardに保存したのですが、保存したものをViewControllerで利用したいです。

構造体にtodo(Stringa型)とrate(Int型)を保存しており、
これをViewControllerへ渡して、TableViewのCellで利用したいです。

TODOリストにレーティング機能がついたようなアプリを作成しており、
rate == 1のときに、入力したテキスト(todo)をセクション1(heart1)へ反映させる、
rate == 1のときに、入力したテキスト(todo)をセクション1(heart2)へ反映させる、といった形で動かしたいと考えております。

やったこと

ViewControllerで利用しようとすると、エラーが出てしまいます。

let heart1 = UserDefaults.standard.data(forKey:"DoneStructList")!.filter {$0.rate==1
} 
→ これを入れるとエラーが出てしまいます。

エラーコード

Value of type 'Data.Element' (aka 'UInt8') has no member 'rate'

該当のソースコード

swift

1 2struct DoneStructList: Codable { 3 4 var list = [DoneStruct]() 5 6 struct DoneStruct: Codable{ 7 8 var todo = String() 9 var rate = Int() 10 11 } 12} 13 14 15class NextViewController: UIViewController { 16 17 18 private var list: [DoneStructList.DoneStruct] = [] 19 20 var rate = 0 21 22 @IBOutlet weak var TextField: UITextField! 23 @IBOutlet weak var Label: UILabel! 24 @IBOutlet weak var heart1: UIButton! 25 @IBOutlet weak var heart2: UIButton! 26 @IBOutlet weak var heart3: UIButton! 27 @IBOutlet weak var heart4: UIButton! 28 @IBOutlet weak var heart5: UIButton! 29 30 let heartFill = UIImage(named: "heart") 31 let heartEmpty = UIImage(named: "emptyheart") 32 33 override func viewDidLoad() { 34 super.viewDidLoad() 35 36 list = getValue() ?? [] 37 38 } 39 40 41 @IBAction func AddButton(_ sender: Any) { 42 43 //配列listの中にある構造体todo,rateそれぞれにTextField.textとrateを保存する。 44 let doneStruct = DoneStructList.DoneStruct(todo: TextField.text!,rate: rate) 45 list.append(doneStruct) 46 save(list: list) 47 48 dismiss(animated: true, completion: nil) 49 } 50 51 //構造体を UserDefaultsへ保存するためにデータ型にエンコード 52 private func serialize(doneStructList:DoneStructList) -> Data?{ 53 do{ 54 let encoder = JSONEncoder() 55 return try 56 encoder.encode(doneStructList) 57 }catch { 58 return nil 59 } 60 } 61 62 //データ型からデコード 63 private func processJson(data: Data) -> [DoneStructList.DoneStruct]? { 64 do { 65 let jsonDecoder = JSONDecoder() 66 let doneStructList = try 67 jsonDecoder.decode(DoneStructList.self,from: data) 68 return doneStructList.list 69 }catch { 70 return nil 71 } 72 } 73 74 //UserDefaultsに保存 75 private func save(list: [DoneStructList.DoneStruct]){ 76 let doneStructList = DoneStructList(list: list) 77 guard let data = serialize(doneStructList: doneStructList)else{ 78 return 79 } 80 UserDefaults.standard.setValue(data, forKeyPath: "DoneStructList") 81 } 82 83 //UserDefaultsから取得 84 private func getValue() -> [DoneStructList.DoneStruct]? { 85 guard let data = UserDefaults.standard.data(forKey: "DoneStructList")else 86 {return nil} 87 return processJson(data: data) 88 } 89 90//以下ハートをタップしたら色が変わる、ラベルに数字が反映される。 91 92 @IBAction func heart1Tapped(_ sender: Any) { 93 94 switch rate { 95 case 1: 96 rate = 0 97 98 heart1.setImage(heartEmpty, for:UIControl.State() ) 99 heart2.setImage(heartEmpty, for:UIControl.State() ) 100 heart3.setImage(heartEmpty, for:UIControl.State() ) 101 heart4.setImage(heartEmpty, for:UIControl.State() ) 102 heart5.setImage(heartEmpty, for:UIControl.State() ) 103 104 105 default: 106 rate = 1 107 108 heart1.setImage(heartFill, for:UIControl.State() ) 109 heart2.setImage(heartEmpty, for:UIControl.State() ) 110 heart3.setImage(heartEmpty, for:UIControl.State() ) 111 heart4.setImage(heartEmpty, for:UIControl.State() ) 112 heart5.setImage(heartEmpty, for:UIControl.State() ) 113 114 } 115 116 Label.text = String(rate) 117 } 118 119 @IBAction func heart2Tapped(_ sender: Any) { 120 121 //ボタンをタップした時の動作 122 rate = 2 123 124 heart1.setImage(heartFill, for:UIControl.State() ) 125 heart2.setImage(heartFill, for:UIControl.State() ) 126 heart3.setImage(heartEmpty, for:UIControl.State() ) 127 heart4.setImage(heartEmpty, for:UIControl.State() ) 128 heart5.setImage(heartEmpty, for:UIControl.State() ) 129 130 Label.text = String(rate) 131 132 } 133 134 @IBAction func heart3Tapped(_ sender: Any) { 135 136 rate = 3 137 138 heart1.setImage(heartFill, for:UIControl.State() ) 139 heart2.setImage(heartFill, for:UIControl.State() ) 140 heart3.setImage(heartFill, for:UIControl.State() ) 141 heart4.setImage(heartEmpty, for:UIControl.State() ) 142 heart5.setImage(heartEmpty, for:UIControl.State() ) 143 144 Label.text = String(rate) 145 146 } 147 148 @IBAction func heart4Tapped(_ sender: Any) { 149 150 rate = 4 151 152 heart1.setImage(heartFill, for:UIControl.State() ) 153 heart2.setImage(heartFill, for:UIControl.State() ) 154 heart3.setImage(heartFill, for:UIControl.State() ) 155 heart4.setImage(heartFill, for:UIControl.State() ) 156 heart5.setImage(heartEmpty, for:UIControl.State() ) 157 158 Label.text = String(rate) 159 160 } 161 162 163 @IBAction func heart5Tapped(_ sender: Any) { 164 165 rate = 5 166 167 heart1.setImage(heartFill, for:UIControl.State() ) 168 heart2.setImage(heartFill, for:UIControl.State() ) 169 heart3.setImage(heartFill, for:UIControl.State() ) 170 heart4.setImage(heartFill, for:UIControl.State() ) 171 heart5.setImage(heartFill, for:UIControl.State() ) 172 173 Label.text = String(rate) 174 175 }
//NextViewControllerで保存した値を以下のViewControllerで使用したい。 class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {   //いったん文字を入れておく。ここにNextViewControllerで保存したtodoを反映させたい。rate==1のときに入力したテキスト(todo)をheart1へ入れる、rate==2のときに入力したテキスト(todo)をheart2へ入れるという形でいれたい。 let heart1 = ["ご飯を食べる"] let heart2 = ["夜更かし","ご飯を食べる"] let heart3 = ["片付けする","ご飯を食べる"] let heart4 = ["タバコを吸う","散歩する","サウナに行く","家で半身浴"] let heart5 = ["温泉に行く","服を買いに行く"] let sectionTitles = ["♡1","♡2","♡3","♡4","♡5"] @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. tableView.delegate = self tableView.dataSource = self } //5つのセクションを用意する func numberOfSections(in tableView: UITableView) -> Int { return 5 }   //sectionTitlesの数だけセクションを用意する func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return sectionTitles[section] } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0{ return heart1.count }else if section == 1{ return heart2.count }else if section == 2{ return heart3.count }else if section == 3{ return heart4.count }else if section == 4{ return heart5.count }else{ return 0 } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell",for: indexPath) if indexPath.section == 0 { cell.textLabel?.text =heart1[indexPath.row] }else if indexPath.section == 1{ cell.textLabel?.text = heart2[indexPath.row] }else if indexPath.section == 2{ cell.textLabel?.text = heart3[indexPath.row] }else if indexPath.section == 3{ cell.textLabel?.text = heart4[indexPath.row] }else if indexPath.section == 4{ cell.textLabel?.text = heart5[indexPath.row] } return cell } @IBAction func AddButton(_ sender: Any) { performSegue(withIdentifier: "next", sender: nil) } }

試したこと

let heart1 = UserDefaults.standard.data(forKey:"DoneStructList")!.filter {$0.rate==1 } 

→ これを入れるとエラーが出てしまいます。
→rate==1のときに入力したテキスト(todo)をheart1へ入れたい
let heart2 = ["夜更かし","ご飯を食べる"]
→rate==2のときに入力したテキスト(todo)をheart2へ入れたい
let heart3 = ["片付けする","ご飯を食べる"]
** →rate==3のときに入力したテキスト(todo)をheart3へ入れたい**
let heart4 = ["タバコを吸う","散歩する","サウナに行く","家で半身浴"]
→rrate==4のときに入力したテキスト(todo)をheart4へ入れたい
let heart5 = ["温泉に行く","服を買いに行く"]
→rate==5のときに入力したテキスト(todo)をheart5へ入れたい

NextViewControllerで保存したStructをViewControllerで使いたいのですが、
どのように実装すればいいでしょうか。

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

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

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

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

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

TsukubaDepot

2021/02/14 13:34

「保存はできている」ということですが、保存はどのようにして行われたのでしょうか。UserDefaults は同一アプリからしかアクセスできないので、一つのコード内に読み書き両方のコードが入っているのが普通かと思います。
zukaibito

2021/02/16 04:14

すみません、以下のコードの記載ができておりませんでした。 以下のコードをViewControllerに記載して、保存しております。 //データ型にエンコード private func serialize() -> Data?{ do{ let encoder = JSONEncoder() let data = try encoder.encode(doneStructList) return data }catch{ return nil } } //UserDefaultsに保存 private func save(){ guard let data = serialize() else { return } UserDefaults.standard.setValue(data, forKeyPath: "DoneStructList") }
TsukubaDepot

2021/02/16 04:24

ほかの回答者にもわかるように、コードはご質問本文を修正し、そちらに追記していただけますでしょうか。 あと、上記のコードは「保存処理だけ」だと思いますが、何か値を入力させ、それを保存するための処理はあるのでしょうか。
zukaibito

2021/02/16 06:09

追記致しました。 申し訳ございません、値の入力はできていないです。 保存したい値は以下になります。 ・NextViewControllerのテキストフィールドに記載された文字を構造体のtodonakamiへ保存。 ・NextViewControllerのLabelの数字を構造体のrateLabelへ保存。 Structの作成〜UserDefaultsへの保存まではNextViewControllerへ記載した方がよろしいでしょうか。
TsukubaDepot

2021/02/16 06:26

上記のコメントですが、NextViewController も拝見しないことにはなんとも言えません。 また、各 ViewController 間の関係もわからないと、おそらくコメントできないと思います。 StroyBoardでいう Initial View Controller はどれになり、どのコントローラにどのような値をわたし、また処理したいのか追加でご説明いただけないでしょうか。必要に応じて別の View Controller のコードも必要となりますので、適宜追加していただければと思います。
zukaibito

2021/02/16 16:31

分かりづらく大変申し訳ございません。 実現したいことの欄、コード含め再度編集いたしました。 NextviewControllerで保存した構造体をviewController(initial viewController)で利用したいです。 構造体にはtodo(string型)とrate(int型)を配列で保存しております。これをviewControllerで適応したいです。 レーティング機能つきのtodoリストのようなものを作成しており、rate==1のときに入力したテキスト(todo)を、viewControllerの♡1のセクション、rate==2のときは♡2のセクション、で利用したいと考えております。 userDefaultsの利用方法等調べてみましたが、解決策がわからないのでご教示頂けると幸いです。 宜しくお願いいたします。
guest

回答1

0

ベストアンサー

Swift

1let heart1 = UserDefaults.standard.data(forKey:"DoneStructList")!.filter {$0.rate==1 2} 

→ これを入れるとエラーが出てしまいます。
Value of type 'Data.Element' (aka 'UInt8') has no member 'rate'

はい、エラーが出ます。

結論から言うと、次の通りです。

UserDefaults.standard.data(forKey:) の戻り値は Data? 型であり、そのアンラップした状態である Data 型には rate というメンバ(プロパティ)が存在しないためです。

そもそも、ViewController では

Swift

1 let heart1 = ["ご飯を食べる"]

という具合に、heart1[String]Stringの配列)型で定義しています。

一方、NextViewController では

Swift

1 private var list: [DoneStructList.DoneStruct] = []

という具合に、[DoneStructList.DoneStruct]型で定義しています。

NextViewController で[DoneStructList.DoneStruct]として Userdefaults に保存したデータを、ViewController で [String] 型である list1 に保存しようとしても、型が全く異なるため保存することはできません。

以下、コード全体の矛盾点です。
これらを全て解決したとしても、細かい点で間違いがあったり、あるいはロジックが不足しているため動かないと思います。
これ以上のことは、以前習われた時に使われたテキストをよく見直し、一つひとつのロジックとしてわからない点を確実に復習したほうがいいかと思います。

データのエンコード、デコードの違い

NextViewController では [DoneStructList.DoneStruct]

Swift

1 let doneStruct = DoneStructList.DoneStruct(todo: TextField.text!,rate: rate) 2 list.append(doneStruct) 3 save(list: list)

という形で、save(list:) というメソッドを使って保存しています。
save(list:) は NextViewController 内部で

Swift

1 //構造体を UserDefaultsへ保存するためにデータ型にエンコード 2 private func serialize(doneStructList:DoneStructList) -> Data?{ 3 do{ 4 let encoder = JSONEncoder() 5 return try 6 encoder.encode(doneStructList) 7 }catch { 8 return nil 9 } 10 } 11 12 //UserDefaultsに保存 13 private func save(list: [DoneStructList.DoneStruct]){ 14 let doneStructList = DoneStructList(list: list) 15 guard let data = serialize(doneStructList: doneStructList)else{ 16 return 17 } 18 UserDefaults.standard.setValue(data, forKeyPath: "DoneStructList") 19 }

と定義していますから、UserDefaults にはシリアライズされた形で保存されています。

一方、今回試された方法は、

Swift

1 let heart1 = UserDefaults.standard.data(forKey:"DoneStructList")!.filter {$0.rate==1 2} 

という具合に、UserDefault に保存されたデータのうち、"DoneStructList"という名前で保存されたデータを Data? 型として直接得ようとしています。

でも、これでは保存した時の型とは違う型となってしまうため、そのままでは利用できません。

NextViewController で

Swift

1 //データ型からデコード 2 private func processJson(data: Data) -> [DoneStructList.DoneStruct]? { 3 do { 4 let jsonDecoder = JSONDecoder() 5 let doneStructList = try 6 jsonDecoder.decode(DoneStructList.self,from: data) 7 return doneStructList.list 8 }catch { 9 return nil 10 } 11 } 12 13 //UserDefaultsから取得 14 private func getValue() -> [DoneStructList.DoneStruct]? { 15 guard let data = UserDefaults.standard.data(forKey: "DoneStructList")else 16 {return nil} 17 return processJson(data: data) 18 }

という具合に getValue()というデコードのためのメソッドを定義してあるので、これと同じような処理を ViewController でも定義して、getValue() というメソッドを使って復号する必要があります。

ただし、通常は実質的に同じメソッドを別々のクラス内で重複して宣言することはやりません。基本的に同じような処理はできるだけ共通化できるよう、独立したクラスや構造体にしたり、あるいは既存の型の拡張(extension)として定義するのですが、そこまで求めていてもおそらく理解が追いつかないと思いますので、まずは「デコード」と「エンコード」の型を一致させる必要があることをよく認識してみてください。

初期化の方法

上記の方法でデコードのための処理を追加したとして

Swift

1 let heart1 = getValue()?.filter {$0.rate==1} ?? []

という処理を ViewController のプロパティ宣言部に記述することはできません(ちなみに、上記のコードはご提示のコードの矛盾点を解決しています)。
仮に記述してもエラーとなってしまいます。

ViewController など、クラスや構造体のプロパティとして宣言する値は、それ自身のインスタンスの初期化が終わるまでは他のメソッド(ここでは getValue())を呼び出すことができないためです。

では、具体的にどのようにすればよいかというと、ViewController の初期化部分では

Swift

1 var heart1: [DoneStructList.DoneStruct] = []

と、配列の型を定義し、とりあえずその配列は空配列として定義したあと、viewDidLoad()などの適切な場所で

Swift

1 heart1 = getValue()?.filter {$0.rate==1} ?? []

という感じで UserDefaults から該当するデータを取得し、保存する必要があります。

ちなみに、getValue() はキーが存在しない場合は nil を返すため、その対策として nil複合演算子を使い、nil の場合は空配列([])で代用するような処理にしています(習ったかわかりませんが)

TableView での表示

ここまで変更できたとしても、

Swift

1 if indexPath.section == 0 { 2 cell.textLabel?.text = heart1[indexPath.row] 3 }

の部分でエラーが出ると思います。

cell.textLabel?.text testString 型ですが、heart1[indexPath.row] の型はDoneStructList.DoneStruct型で、やはり型が一致しません。

実際に表示させたいのは、DoneStruct 型にある todo なので

Swift

1 if indexPath.section == 0 { 2 cell.textLabel?.text = heart1[indexPath.row].todo 3 4 }

という具合に記述しなければいけないと思います。

###構造体定義の問題

なるべく話が通りやすいよう、ここまでは DoneStructList.DoneStruct という型で話を進めてきましたが、実際はDoneStructList という型が必要ないため DoneStruct という型だけで処理を進めるべきだと思います。

なぜこのような処理にしたのかわかりませんが、それは参考にされたテキストでそのような教え方をしていたのかもしれませんし、現時点では処理上の不都合はでないので放置しますが、実際は無駄な処理が増えていることだけは認識しておいた方がいいと思います。

###初期データの問題

さて、おそらくここまで各部分に手を加え、無事コンパイルが通ったとしても、TableView には何も表示されないと思います。

それは当然のことであり、当初 ViewControllerで定義していた

Swift

1 let heart1 = ["ご飯を食べる"]

に相当する、初期データを保存するための処理が消えたためです。

しかし、ご質問の内容はあくまでも UserDefailts に保存された内容を取り出すことであり、その目的であればこの結果になってしまって当然だと考えるしかありません。

具体的に何をどのように処理したいのか、初期データは何で、そのうちどのデータを変更したいのか、またそれぞれの View Controller 間では何を受けわたししたいのかという、基本的なデータの流れを定義できるのは質問者さんご自身でしかないため、そこについては改めてよく考えていただく必要があります。

以前も言ったかもしれませんが、習われたとおっしゃっている講座では、最終的に色々な手法を使われたのだと思いますが、それらは一つひとつの機能をしっかり理解した上で使わないことには、結局どこに根本的な原因があるのか突き止めるのが難しくなる一方です。

もちろん、小さなところばかり見ていても進まないので、大局的な見方も必要ですが、それぞれ適度なバランスを保ちながら学習する必要もあります。

全体的な流れとして話がわかったのであれば、細かい流れをおってわからない処理を理解する。あるいは、全体的な流れからよくわかっていなければ、やはり細かい処理の中で理解できる部分から理解を進める。

また、理解が進んだかどうかは、いきなり大きなコードを書くのではなく、自分が理解できる範囲でのごく小さなコードを記述し、エラーを少しずつ解決しながら大きなコードを書く、というのが基本原則ですので、それに従われるのがいいかと思います。

投稿2021/02/16 23:37

TsukubaDepot

総合スコア5086

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

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

zukaibito

2021/02/17 11:26

ご丁寧な回答を頂き、ありがとうございます。 おっしゃる通り、全体の流れをよく理解できていないまま作成しておりました。 再度基礎から復習し、それぞれの機能の役割を理解しながら、より簡易的なアプリ制作から進めていきたいと思います。 ご指摘頂きまして本当にありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

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

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

質問する

関連した質問