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

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

詳細はこちら
iOS

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

保存

保存(save)とは、特定のファイルを、ハードディスク等の外部記憶装置に記録する行為を指します。

Xcode

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

Swift

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

Q&A

解決済

2回答

4266閲覧

SwiftのカスタムクラスをUserDefaultsで保存して取り出したい

tttttttnaga

総合スコア7

iOS

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

保存

保存(save)とは、特定のファイルを、ハードディスク等の外部記憶装置に記録する行為を指します。

Xcode

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

Swift

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

0グッド

0クリップ

投稿2019/10/16 03:25

編集2019/10/16 03:33

前提・実現したいこと

todoとtimerを組み合わせたアプリを作成しています。
登録したtodoに対してかかった時間を記録して、別の画面のtableViewで
todoを実施した日付でセクションを分けて表示させたいです。

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

自作したサンプルデータを使ってtableViewに表示させることはできたのですが、
UserDefaultsを使ってtodoや時間を保存して受け渡そうとしたときに
取り出されるデータが空になってしまいます。

受け渡したいデータは、以下の4つで、これらを格納するカスタムクラスをUserDefaultsで取り扱いたいです。

1.todo : String
2.time1 : Int
3.time2 : Int
4.date : Date

該当のソースコード

swift

1import Foundation 2 3class Content:NSObject, NSCoding { 4 5 let todo: String 6 let time1: Int 7 let time2: Int 8 let date: Date 9 10 init(todo:String, time1:Int, time2:Int, date:Date) { 11 self.todo = todo 12 self.time1 = time1 13 self.time2 = time2 14 self.date = date 15 } 16 17 func encode(with aCoder: NSCoder) { 18 aCoder.encode(self.todo, forKey: "todo") 19 aCoder.encode(self.estimate, forKey: "time1") 20 aCoder.encode(self.actual, forKey: "time2") 21 aCoder.encode(self.date, forKey: "date") 22 } 23 24 required init?(coder aDecoder: NSCoder) { 25 todo = aDecoder.decodeObject(forKey: "todo") as! String 26 time1 = aDecoder.decodeInteger(forKey: "time1") 27 time2 = aDecoder.decodeInteger(forKey: "time2") 28 date = aDecoder.decodeObject(forKey: "date") as! Date 29 } 30}

swift

1import UIKit 2 3class ViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource { 4 5 // 保存するデータのオブジェクトとそれを格納する配列を用意 6 var content = [Content]() 7 var contents = [[Content]]() 8 9 10 ~~~中略~~~ 11 12 13 // doneボタンをクリックした時の処理 14 @IBAction func clickDoneButton(_ sender: Any) { 15 timer.invalidate() 16 17 let todoString:String = todo.text! 18 var time1Int:Int = Int() 19 var time2Int:Int = Int() 20 let now:Date = Date() 21 22 let index = timePickerOption.firstIndex(of: timeLabel.text!) 23 time1Int = convertedTimePickerOption[index!] 24 time2Int = time1Int - count 25 26 // done!押下時の時間を"yyyy/mm/dd"に変換する 27 let f = DateFormatter() 28 f.timeStyle = .none 29 f.dateStyle = .medium 30 f.locale = Locale(identifier: "ja_JP") 31 f.dateFormat = "yyyy/MM/dd" 32 let s = f.string(from: now) 33 34 // contentに書き込み、contentsにappend 35 content = [Content(todo: todoString, time1: time1Int, time2: time2Int, date: f.date(from: s)!)] 36 contents.append(content) 37 38 // UserDefaultsに保存 39 let encodedContents = try? NSKeyedArchiver.archivedData(withRootObject: contents, requiringSecureCoding: false) 40 UserDefaults.standard.set(encodedContents, forKey: "contents") 41 UserDefaults.standard.synchronize() 42 43 // segueでRecordVCに遷移する 44 performSegue(withIdentifier: "record", sender: nil) 45 46 47 }

swift

1 override func viewDidLoad() { 2 super.viewDidLoad() 3 4 recordTableView.delegate = self 5 recordTableView.dataSource = self 6 7 prepare() 8 } 9 10 private func prepare() { 11 // 日付を用意して、"yyyy/MM/dd"に変換 12 let f = DateFormatter() 13 f.locale = Locale(identifier: "en_US_POSIX") 14 f.dateFormat = "yyyy/MM/dd" 15 16 var contents: [Content]! 17 let contentsData = UserDefaults.standard.object(forKey: "contents") as? Data 18 guard let t = contentsData else { return } 19 let unArchiveData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(t) 20 contents = unArchiveData as? [Content] ?? [Content]() 21 22 print(contents!) // -> 結果が空[]になってしまう 23 24 todos = Dictionary(grouping: contents) { content -> Date in 25 return content.date 26 } 27 .reduce(into: [Date: [Content]]()) {dic, tuple in 28 dic[tuple.key] = tuple.value.sorted { $0.date < $1.date } 29 } 30 31 // 日付順を保持するための配列 32 dateOrder = Array(todos.keys).sorted { $0 > $1 } 33 34 } 35

試したこと

SwiftでAttempt to insert non-property list objectが出た時の対処について
swift4でカスタムオブジェクトをNSUserDefaultsに保存・読込する

こちらを参考にUserDefaultsへの保存を試みたのですが、うまく値を取り出せません。

contentsにappendするまでは値を入れられているようなのですが、
UserDefaultsへの保存、取り出しで空になってしまっているように思えます。
使い方が不適切なのでしょうか。

よろしくお願いいたします。

補足情報(FW/ツールのバージョンなど)

xcode Version: 11.1

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

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

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

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

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

guest

回答2

0

ベストアンサー

UserDefaultsに保存しているデータの型はArray<Array<Content>>ですが取り出すときに

swift

1contents = unArchiveData as? [Content] ?? [Content]()

Array<Content>にキャストしようとしています。
当然キャストできないのでnilになります。


型の処理とかはSwiftではやらなくていいならやらないという方針でいったほうがいいです。
ですのでいまならJSONEncoder/JSONDecoderを使う方がいいかな

swift

1// NSCoding ではなく Codableにする 2// NSCodingのためだけにNSObjectにしていたのならNSObjectは不要。 struct Content: CodableでもOK 3class Content: NSObject, Codable { 4 5 let todo: String 6 let time1: Int 7 let time2: Int 8 let date: Date 9 10 init(todo:String, time1:Int, time2:Int, date:Date) { 11 self.todo = todo 12 self.time1 = time1 13 self.time2 = time2 14 self.date = date 15 } 16}

UserDefaultsを拡張

swift

1extension UserDefaults { 2 func setCodableObject<T: Encodable>(item: T, forKey defaultName: String) { 3 guard let data = try? JSONEncoder().encode(item) else { 4 print("Can not Encode to JSON.") 5 return 6 } 7 8 set(data, forKey: defaultName) 9 } 10 11 func codableObject<T: Decodable>(forKey defaultName: String) -> T? { 12 guard let data = data(forKey: defaultName) else { return nil } 13 guard let object = try? JSONDecoder().decode(T.self, from: data) else { 14 print("Can not Decode from JSON.") 15 return nil 16 } 17 18 return object 19 } 20}

としておけば、

swift

1// 保存 Array<Content>のままでOK 2UserDefaults.standard.setCodableObject(content , forKey: "contents") 3 4// 読み出し Optionl<Array<Content>>で取得できる 5let contents: [Content]? = UserDefaults.standard.codableObject(forKey: "contents")

と出来ます。


それと、現在はUserDefaults.standard.synchronize()は無意味なので使わないでください。


さらに、ここまで書いておいてなんですが、アプリケーション内でのデータの受け渡しにUserDefaultsを使うのは推奨されません。


GitHubなどにはKeyに型情報を持たせてさらにデータを扱いやすくしたUserDefaultsのラッパーなどもあるので調べてみるといいです。

投稿2019/10/16 07:02

編集2019/10/16 07:06
MasakiHori

総合スコア3391

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

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

tttttttnaga

2019/10/16 07:22

ご回答いただきありがとうございます!! 冒頭ご指摘の通り、保存する型と取り出す型が異なっておりました。 その点を修正したところ、とりあえずは期待する動きができました。 また推奨方法まで詳しくコメントいただきありがとうございます。 勉強不足で申し訳ありません、いただいた情報を調べてみます! >さらに、ここまで書いておいてなんですが、アプリケーション内でのデータの受け渡しにUserDefaultsを使うのは推奨されません。 todo(というよりも実施事項というイメージですが)を保持しておきたかったのでUserDefaultsを使うと思ったのですが、受け渡し含め保存すること自体も他の方法が良いということでしょうか? お手隙でしたらご回答いただけますとより勉強になります。 よろしくお願いいたします。
MasakiHori

2019/10/16 08:15

https://developer.apple.com/documentation/foundation/userdefaults リファレンスにある通り「設定」などで使用されるユーザーごとの設定を保存する場所です。 例えばTodoですと日付は表示するけど時間は表示しないなどの情報が考えられます。 なので汎用データストレージとして使用することは推奨されていません。 ただし、汎用データストレージとして使ってもリジェクトされないですし爆発もしないのでそこはプログラマさんがどう考えるか次第です。 ネットでよくUserDefaultsがそのように使われているのは、単にお手軽に使えるから、だけだと思われます。 お手軽が第一と考える場合はUserDefaultsを使うのはありでしょう。
tttttttnaga

2019/10/16 08:44

ありがとうございます! お手軽だから、というのがしっくりきました。 Udemyの講座ではfirebaseを使っている事例があって、認証や画像等重いデータを扱うときは外部のDBやストレージを使うのかな〜くらいの飲み込み方をしていて、どういうケースでどういう保存方法を使うのかまだわかっておらず。。 調べてみてやはりふに落ちない、解決方法がわからないときは改めて質問を立てさせていただきます! ご丁寧に解説いただきありがとうございました!
guest

0

適当に以下のようなテストプログラムを書いて検証してみましたが、特に問題なく動作しているように見えます。

swift

1import UIKit 2 3class Content:NSObject, NSCoding { 4 5 let todo: String 6 let time1: Int 7 let time2: Int 8 let date: Date 9 10 init(todo:String, time1:Int, time2:Int, date:Date) { 11 self.todo = todo 12 self.time1 = time1 13 self.time2 = time2 14 self.date = date 15 } 16 17 func encode(with aCoder: NSCoder) { 18 aCoder.encode(self.todo, forKey: "todo") 19 aCoder.encode(self.time1, forKey: "time1") 20 aCoder.encode(self.time2, forKey: "time2") 21 aCoder.encode(self.date, forKey: "date") 22 } 23 24 required init?(coder aDecoder: NSCoder) { 25 todo = aDecoder.decodeObject(forKey: "todo") as! String 26 time1 = aDecoder.decodeInteger(forKey: "time1") 27 time2 = aDecoder.decodeInteger(forKey: "time2") 28 date = aDecoder.decodeObject(forKey: "date") as! Date 29 } 30} 31 32 33class ViewController: UIViewController { 34 35 override func viewDidLoad() { 36 super.viewDidLoad() 37 // Do any additional setup after loading the view. 38 } 39 40 func save() { 41 print("save") 42 let contents = [ 43 Content(todo: "TEST00", time1: 1, time2: 2, date: Date()), 44 Content(todo: "TEST01", time1: 1, time2: 2, date: Date()), 45 ] 46 47 let encodedContents = try? NSKeyedArchiver.archivedData(withRootObject: contents, requiringSecureCoding: false) 48 49 UserDefaults.standard.set(encodedContents, forKey: "contents") 50 } 51 52 func load() { 53 print("load") 54 var contents: [Content]! 55 let contentsData = UserDefaults.standard.object(forKey: "contents") as? Data 56 guard let t = contentsData else { return } 57 let unArchiveData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(t) 58 contents = unArchiveData as? [Content] ?? [Content]() 59 60 print(contents.count) 61 print(contents[1].todo) 62 } 63 64 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { 65 save() 66 load() 67 } 68}

出力:

text

1save 2load 32 4TEST01

載せてもらったソース以外の場所に原因があるのではないでしょうか?
(UserDefaultsを全消しするコードが入っているとか)

投稿2019/10/16 05:50

takabosoft

総合スコア8356

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

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

tttttttnaga

2019/10/16 07:12

ご回答いただきありがとうございます!! 自分のコードでも色々printしながら確認していたのですが いただいたコードを参考にprint(contents[0].todo)と打とうとしたところ todoがcontents[]の名前ではないよと出てきました。 改めて確認したところ変数の型の指定が間違っていたようです。。。 【before】 var content = [Content]() var contents = [[Content]]() 【after】 var content = Content(todo: "", estimate: 0, actual: 0, date: Date()) var contents = [Content]() と変更したところ無事動きました!! よくわからず色々な記事を継ぎ接ぎして作っていましたが少し理解が深まりました! ありがとうございました!!
takabosoft

2019/10/17 00:32

MasakiHoriさんの方でいろいろ言ってもらっているのでほとんど言うことがないですが、選択肢としては、自分でDocumentフォルダにJSON等で保存・読み込みする、というものもあります。外部DB,ローカルDB,ローカルJSON(、ローカルバイナリ),どれにするか悩んでみてください。
tttttttnaga

2019/10/18 01:31

追記いただきありがとうございます!! 別の方法でできないかも検討してみます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

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

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

質問する

関連した質問