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

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

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

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

Q&A

解決済

2回答

1140閲覧

Swift5: UserDefaultsで任意の型のデータを保存、参照したい

tmsah

総合スコア101

Swift

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

0グッド

0クリップ

投稿2020/04/24 13:29

Swift5初心者です.ストーリーボードは使っておりません.
UserDefaultsを使って自らが定義した型のデータを保存、参照したいと考えております。

現在ペイントアプリを開発しており、画面に絵を書いている途中でアプリを終了した場合に、次回起動時に途中まで描いていた絵を再度表示させたいと考えております。
その際に保存しておきたいデータはペンの太さ(strokeWidth)、ペンの色(color)、描いた線([points])で、これらをメンバに持つ構造体Lineを定義し、Line型のデータ配列linesとして絵を保持しています。

struct Line { let strokeWidth: Float let color: UIColor var points: [CGPoint] } var lines = [Line]()

描画の処理はtouchesMoveで行っておりlinesオブジェクトが正しく動作していることは確認済みです。
絵を描いている途中でアプリを終了した場合に備えて、ペンを画面から離すたびにUserDefaultslinesを保存する実装を行いました。

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { UserDefaults.standard.set(data:lines, forKey: "lines") } }

そしてアプリの起動時にlinesオブジェクトが保存されているか確認し、保存されていればそれを画面に描画します。

if let lastLines: [Line] = UserDefaults.standard.object(key: "lines") as? [Line] { lines = lastLines setNeedsDisplay() }

これらの実装を行っても、再起動前の絵を描画することはできませんでした。
調べてみたところ、UserDefaultsは保存できるデータ型に制限(StringBool等)があり、自らが定義した型のデータが保存できるわけではないとのことでした。
カスタムクラスでの保存方法としてこちらのサイト等を参考に解決を試みましたが、やはり使用できる型の制限があるようでうまくいきませんでした。
このアプリはWi-Fi環境の無い場所での使用を想定しているため、データベースやクラウドなどを用いることもできません。

どのような実装を行えばUserDefaultsに任意の型のデータを保存できるのでしょうか。
この問題を解決するコード例や、機能実現の別のアイディア等を教えていただきたいです。
よろしくお願いいたします。

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

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

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

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

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

guest

回答2

0

ベストアンサー

お気づきのように、カスタムクラスはそのままではUserDefaultsに保存することができません

カスタムクラスを保存する方法は上記の回答に2つ挙げられています。

一つ目は、MasakiHoriさんが示された方法です。
カスタムクラス内のプロパティが全てCodableに準拠していれば、JSONにすることで保存することができます。

もう一つは、同じ回答で私が示した方法です。
こちらはやや処理が面倒ですが、Codableでないクラスや構造体も保存することが可能です。

ところで、私が提案している方法は「こちらのサイト」で示されている方法と同じ方法です。
既にお試しになってうまくゆかなかったということですが、どのようにうまくゆかなかったのでしょうか。
それがわかればもう少しお手伝いできるかもしれません。

投稿2020/04/24 14:24

TsukubaDepot

総合スコア5086

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

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

tmsah

2020/04/25 03:29

回答ありがとうございます。 「こちらのサイト」に記された手法を用いた実装は次の通りです。 まず、サイトと同様に`Line`クラスを定義した後、`Line`型のデータ配列`lines`を宣言しました。 ``` class Line: NSObject, NSCoding { var strokeWidth: Float var color: UIColor var points: [CGPoint] init(strokeWidth: Float, color: UIColor, points: [CGPoint]) { self.strokeWidth = strokeWidth self.color = color self.points = points } required init?(coder aDecoder: NSCoder) { self.strokeWidth = aDecoder.decodeObject(forKey: "strokeWidth") as! Float self.color = aDecoder.decodeObject(forKey: "color") as! UIColor self.points = aDecoder.decodeObject(forKey: "points") as! [CGPoint] } func encode(with aCoder: NSCoder) { aCoder.encode(strokeWidth, forKey: "strokeWidth") aCoder.encode(color, forKey: "color") aCoder.encode(points, forKey: "points") } } var lines = [Line]() ``` そしてそれに合わせて`touchesEnded`の処理を改変しました。 ``` override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { let lastLines = try! NSKeyedArchiver.archivedData(withRootObject: lines, requiringSecureCoding: false) UserDefaults.standard.set(lastLines, forKey: "lines") } } ``` また、再起動時のロードには`loadLines`メソッドを定義し、これを用いてデータの参照を試みました。 ``` func loadLines() -> [Line]?{ if let lastLines = UserDefaults.standard.data(forKey: "lines") { let line = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(lastLines) as! [Line] return line }else { return nil } } ... if let lastLines: [Line] = loadLines() { lines = lastLines setNeedsDisplay() } ... ``` `NSKeyedArchiver` と `NSKeyedUnarchiver` に関して、iOS 12以降のOSを対象に記述する例として[このサイト](https://fromatom.hatenablog.com/entry/2019/02/01/174830)を参考に改変しました。 以上の実装をしたところ、`touchesEnded`での処理ではエラーは出ませんでした(正しくデータが保存されているのかは分かりませんが...)。 そして再起動時に`Line`クラスの`required init`のタイミングで次のエラーが出ます。 ``` Fatal error: Unexpectedly found nil while unwrapping an Optional value ``` `self.strokeWidth`の行に対して表示されましたが、他の変数であっても同様です。 データがnilになるということから、保存の段階でうまくできていないということなのでしょうか。 お忙しいところ恐縮ですがご回答いただけると幸いです。 よろしくお願いいたします。
TsukubaDepot

2020/04/25 04:34

落ちる原因は self.strokeWidth = aDecoder.decodeObject(forKey: "strokeWidth") as! Float の部分ですね。 元々のデータ型が違うため nil が返り、それを強制アンラップしているので落ちています。 Floatとして保存されたものは、 self.strokeWidth = aDecoder.decodeFloat(forKey: "strokeWidth") とFloatとして出してあげる必要があります(そういう意味では私の過去の回答は不十分ですね)。
tmsah

2020/04/25 08:40

回答ありがとうございます。 self.strokeWidthの部分を修正したところ、無事期待通りの挙動が確認できました。 公式ドキュメント(https://developer.apple.com/documentation/foundation/nscoder/1418185-decodeobject)にメソッドが用意されているデータ型の場合はそれを使わなければならないのですね。 勉強になりました。 丁寧な回答ありがとうございました。
guest

0

出来るかわかりませんが、次のようなアイディアはどうでしょうか。

Swift

1//[Line]と持たずにメンバーを配列にしてみます。 2var strokeWidths = [float] 3var colors [UIColor] 4var pointsArray: [[CGPoint]] 5UserDefaults.standard.set(data:strokeWidths, forKey: "strokeWidths") 6UserDefaults.standard.set(data:colors, forKey: "colors") 7UserDefaults.standard.set(data:pointsArray, forKey: "pointsArray") 8//読み込んだ時は、配列のインデックスごとにLine構造体を作ります。 9

投稿2020/04/24 14:02

freemann

総合スコア264

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

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

tmsah

2020/04/25 08:45

回答ありがとうございます。 詳しく調べてみますと、UIColorやCGPointなどのデータ型であっても素直にUserDefaultsでの保存はできないようでした。 そこで今回はNSData型に変換することでこれを解決しました。 ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問