【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

 

詳しく言うと、

なので、

  • 他の型を使用する
  • Codableを使用する

で実装した方がいいらしい。

 

(引用元)

stackoverflow.com

stackoverflow.com

 

タプル→辞書、に変換

 

注意点として、タプルは必ずラベルを記述する必要があり、辞書型のように[String : Any]と記述することはできない。

 

なので、

  • 保存用のタプルを宣言
  • typealiasでタプルを別名で宣言

のいずれかを実装するしかない。

 

(typealiasについては下記を参照)

qiita.com

 

    //タプル -> 辞書に変換
    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]))

 

結果

 

f:id:nekokichi_yos2:20200825105558g:plain

 

ソースコード

 

f:id:nekokichi_yos2:20200825103831p:plain

 

f:id:nekokichi_yos2:20200825110940p:plain

 

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
    }
    
}

 

参考

 

qiita.com

qiita.com