【Swift】UserDefaultにタプルを辞書で保存する
どうも、ねこきち(@nekokichi1_yos2)です。
タプルをUserDefaultに保存する方法を模索してて、良い方法を見つけたので、備忘録として残します。
解説
UserDefaultが保存できるのは、
- Int
- String
- Array
- Dictionary
- Double
- Float
- Bool
- URL
なので、タプル(Tuple)は保存できない。
したがって、タプル→対応している型、に変換する必要がある。
辞書型(Dictionary)は、タプルと同様に、キー(もしくはラベル)で値を参照でき、見た目でも似てるので、辞書の方が都合が良い。
Data型で保存できないのか?
下記のコードで、Data型に変換を試みました。
let tuple = (text:"fdsafs", bool:true) let data:Data = try! NSKeyedArchiver.archivedData(withRootObject: tuple, requiringSecureCoding: false) UserDefaults.standard.set(data, forKey: "data")
しかし、下記のエラーが発生。
Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=4866 "Caught exception during archival: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600002558880
詳しく言うと、
- NSKeyedArchiverはObjective-C専用のメソッド
- Objective-Cに非対応のデータはアーカイブできない
なので、
- 他の型を使用する
- Codableを使用する
で実装した方がいいらしい。
(引用元)
タプル→辞書、に変換
注意点として、タプルは必ずラベルを記述する必要があり、辞書型のように[String : Any]と記述することはできない。
なので、
- 保存用のタプルを宣言
- typealiasでタプルを別名で宣言
のいずれかを実装するしかない。
(typealiasについては下記を参照)
//タプル -> 辞書に変換 func convertToDictionaryFromTuple(tuple: (text:String, num:Int)) -> [String: Any] { return [ "num" : tuple.num, "text" : tuple.text ] }
textListForUD.append(convertToDictionaryFromTuple(tuple: textList[i]))
辞書→タプル、に変換
返り値にタプルを指定する際には、ラベルを定義する必要があり。
//辞書 -> タプルに変換 func convertToTupleFromDictionary(dictionary: [String: Any]) -> (text:String, num:Int) { return ( num : dictionary["num"] as! Int, text : dictionary["text"] as! String ) }
textList.append(convertToTupleFromDictionary(dictionary: checkListData[i]))
結果
ソースコード
import UIKit class TupleAndDictionaryViewController: UIViewController,UITableViewDelegate,UITableViewDataSource { @IBOutlet weak var tableView: UITableView! private let ud = UserDefaults.standard //リスト(タプルの配列) private var textList:Array<(text:String, num:Int)> = [] override func viewDidLoad() { super.viewDidLoad() //UserDefaultからデータを取得 if let checkListData = ud.object(forKey: "checkList") as? [[String: Any]] { for i in 0..<checkListData.count { textList.append(convertToTupleFromDictionary(dictionary: checkListData[i])) } } //tableViewの設定 tableView.delegate = self tableView.dataSource = self tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "customcell") } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return textList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //CustomCellを生成 let cell = tableView.dequeueReusableCell(withIdentifier: "customcell", for: indexPath) as! CustomCell //CustomCell内のUIに値を入れる cell.configure(text: textList[indexPath.row].text, num: textList[indexPath.row].num) return cell } //タプル -> 辞書に変換 func convertToDictionaryFromTuple(tuple: (text:String, num:Int)) -> [String: Any] { return [ "num" : tuple.num, "text" : tuple.text ] } //辞書 -> タプルに変換 func convertToTupleFromDictionary(dictionary: [String: Any]) -> (text:String, num:Int) { return ( num : dictionary["num"] as! Int, text : dictionary["text"] as! String ) } @IBAction func segueToAddItemVC(_ sender: Any) { performSegue(withIdentifier: "add", sender: nil) } @IBAction func unwindToVC(_ unwindSegue: UIStoryboardSegue) { //AddCheckItemで追加ボタンが押下された時 if unwindSegue.identifier == "addItem" { //新規データを追加 let addCheckItemVC = unwindSegue.source as! AddItemViewController textList.append(addCheckItemVC.textItem) //UserDefaultに保存 var textListForUD = [[String: Any]]() for i in 0..<textList.count { textListForUD.append(convertToDictionaryFromTuple(tuple: textList[i])) } ud.set(textListForUD, forKey: "checkList") //初期化 textListForUD = [[String: Any]]() //tableViewを更新 tableView.reloadData() } } }
class AddItemViewController: UIViewController { @IBOutlet private weak var itemTextField: UITextField! @IBOutlet private weak var itemNumField: UITextField! private(set) var textItem:(text:String, num:Int)! func showAlert(title:String, message:String) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } @IBAction func addItem(_ sender: Any) { if itemTextField.text == "" || itemNumField.text == "" { showAlert(title: "エラー", message: "1文字以上の文字を入力してください") } else if Int(itemNumField.text!) == nil { showAlert(title: "エラー", message: "数字を入力してください") } else { let itemText = itemTextField.text! let itemNum = Int(itemNumField.text!) ?? 0 //入力値を追加用のデータに代入 textItem = (text:itemText, num:itemNum) //ViewControllerに戻る performSegue(withIdentifier: "addItem", sender: nil) } } }
import UIKit class CustomCell: UITableViewCell { @IBOutlet private weak var cellNumLabel: UILabel! @IBOutlet private weak var cellTextLabel: UILabel! func configure(text:String, num:Int) { cellNumLabel.text = String(num) cellTextLabel.text = text } }
参考