【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 } }
参考
【Swift】unWindSegueで前画面に戻りつつ値を渡す
どうも、ねこきち(@nekokichi1_yos2)です。
今回は、unWindSegue、の使用方法を書いていきます。
解説
手順は、
- 画面を用意する(2個以上)
- 前画面にunWindSegueを記述
- StoryBoardで遷移元にunWindSegueを接続
- 遷移元でunWindSegueを実行
画面を用意する(2個以上)
今回の画面構成は下記。
- 前画面:unWindSegue1
- 遷移元:unWindViewController
前画面にunWindSegueを記述
前画面(unWindSeuge1)にunWindSegueを記述します。
(@IBActionと書かれてますが、UI部品と接続することはありません。)
また、引数のunwindSegueには、下記のプロパティが用意されています。
- .source:遷移元
- .destination:遷移先
今回の場合、遷移元がunWindViewControllerなので、unWindSegue.source、にはunWindViewControllerが格納されます。
そして、遷移元の変数に直接、アクセスできます。
(下記では、遷移先のtextFieldの値をlabelに代入しています。)
@IBOutlet weak var label: UILabel! @IBAction func unwind(_ unwindSegue: UIStoryboardSegue) { //unWindViewController guard let source = unwindSegue.source as? unWindViewController else { return } label.text = source.textField.text }
StoryBoardで遷移元にunWindSegueを接続
前提として、遷移先にunWindSegueを記述する必要があります。
(でないと、接続できません。)
StoryBoard上で、unWindViewControllerをExitに接続します。
先ほど、遷移先で作ったunWindSegueを選択します。
次は、unWindSegueを選択し、
Identifierを設定します。
(今回はexit)
遷移元でunWindSegueを実行
あとは、遷移元でperformSegueを実行するだけです。
(withIdentifierには、StoryBoard上で設定したIdentifierを記述します。)
@IBAction func back(_ sender: Any) { performSegue(withIdentifier: "exit", sender: nil) }
結果
ソースコード
import UIKit class unWindSegue1: UIViewController { @IBOutlet weak var label: UILabel! @IBAction func unwind(_ unwindSegue: UIStoryboardSegue) { //unWindViewController guard let source = unwindSegue.source as? unWindViewController else { return } label.text = source.textField.text } }
import UIKit class unWindViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBAction func back(_ sender: Any) { performSegue(withIdentifier: "exit", sender: nil) } }
参考
[Xcodeアプリ開発入門] Part32 Modal遷移時にデータを前の画面に戻す方法
【Swift】UserDefaultに非対応のデータを保存する
どうも、ねこきち(@nekokichi1_yos2)です。
今回は、UserDefaultでAny型に含まれる、UserDefaultには非対応のデータを扱う方法を書いていきます。
解説
Any型で保存できないの?
一部のデータは保存できました。
例えば、普通の辞書や配列は問題ありませんでした。
// [String] UserDefaults.standard.set(["gsgs","gs","56252"], forKey: "array") // [String : Array] UserDefaults.standard.set(["color":[2,3,4], "afa":[2,5,2]], forKey: "array") // [String : [String : Int]] UserDefaults.standard.set(["dic1":["2":2],"dic2":["1":1]], forKey: "array")
しかし、UILabelの配列や、異なる型を格納した辞書だと、エラーが出ます。
let dic:[String:Any] = [ "color":UIColor.red, "bool":bool ] UserDefaults.standard.set(dic, forKey: "color_bool")
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object {
bool = 1;
color = "UIExtendedSRGBColorSpace 1 0 0 1";
} for key color_bool'
残念ながら、Any型でも、あらゆるデータを保存することはできないようです。
じゃあどうするか、Data型に変換すれば良いのです。
Data型に変換して、Data型から元の型に戻す
Data型への変換は、NSKeyedArchiver.archivedData、を使用します。
(requiringSecureCodingの値は、true,false、のどちらでも問題ありません。)
let data:Data = try! NSKeyedArchiver.archivedData(withRootObject: 保存したいデータ, requiringSecureCoding: false)
そして、保存したデータを取り出す際は、NSKeyedArchiver.unarchiveTopLevelObjectWithData、で取り出します。
(変数に入れる場合、型を指定する必要があります。)
if let data = UserDefaults.standard.data(forKey: "キー") { let dic = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:Any]
let text = dic["bool"] as! Bool
}
結果
ソースコード
import UIKit class ViewController2: UIViewController { @IBOutlet weak var label: UILabel! @IBAction func display(_ sender: Any) { if let data = UserDefaults.standard.data(forKey: "color_bool") { let dic = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String:Any] let text = dic["bool"] as! Bool label.text = "\(text)" label.backgroundColor = dic["color"] as! UIColor } } @IBAction func red(_ sender: Any) { setData(.red, true) } @IBAction func blue(_ sender: Any) { setData(.systemBlue, false) } @IBAction func yellow(_ sender: Any) { setData(.yellow, true) } func setData(_ color:UIColor, _ bool:Bool) { let dic:[String:Any] = [ "color":color, "bool":bool ] let data:Data = try! NSKeyedArchiver.archivedData(withRootObject: dic, requiringSecureCoding: false) UserDefaults.standard.set(dic, forKey: "color_bool") } }
参考
【Swift】GeccoでスポットライトなUIを実現
どうも、ねこきち(@nekokichi1_yos2)です。
アプリの初回起動時、チュートリアルが流す場合、ユーザーにUIの説明をする必要があります。
その際、スポットライトでUIを照らして、テキストで説明するチュートリアルをたまに見かけます。
(参考:https://www.mobile-patterns.com/coach-marks)
そこで今回は、スポットライトを表示させるライブラリ - Gecco - の実装方法を書いていきます。
注意
Geccoは4,5年前にGitHubでの更新が止まっているので、今の環境で動くかどうか保証はできません。
本記事を投稿した時点では問題なくビルドできましたが、今後のSwiftやXcodeのバージョンアップでエラーが起こる可能性があるかもしれないことにご注意を。
解説
手順は、下記の4つ。
- Geccoを導入
- 2つのViewControllerを作る
- スポットライトとテキストを用意
- スポットライトとテキストアニメーションを設定
Geccoを導入
podfile経由でインストール。
↓
2つのViewControllerを作る
Geccoの実装に必要なのは、
- Geccoの設定(スポットライトの位置や表示設定など)
- Geccoの実装(スポットライトを表示させる)
の2つ。
今回は、設定用をGeccoVCとして、
(クラスは、SpotlightViewController、と定義してください。)
import UIKit import Gecco class GeccoVC: SpotlightViewController {
//各UILabelの座標データ var uiLabels_frames = [CGRect()] //画面サイズ let screenSize = UIScreen.main.bounds.size
}
実装用をDisplayGeccoとします。
(先に、スポットライトで指し示すUIを用意しておきます。)
import UIKit class DisplayGecco: UIViewController { @IBOutlet weak var button1: UIButton! @IBOutlet weak var button2: UIButton! @IBOutlet weak var button3: UIButton! }
DisplayGeccoではGeccoを実行するコードを書き、GeccoVCではスポットライトやテキストを表示するのに必要な設定を施していきます。
スポットライトとテキストを用意
まず、必要なのは、
- スポットライトのframe
- スポットライトを進行させるためのdelegate
- テキストを生成
- テキストを表示させる処理
の4つ。
スポットライトのframe
スポットライトでUIを照らすので、そのUIの座標を取得する必要があります。
よって、DisplayGeccoのUIButtonを指し示すので、
- 各UIButtonの座標を取得
- GeccoVCに渡す
- スポットライトを生成
を実行します。
各UIButtonのCGRectを配列に格納し、
func passCGRect() -> [CGRect] { var arrayCGRect = [CGRect]() arrayCGRect.append(button1.frame) arrayCGRect.append(button2.frame) arrayCGRect.append(button3.frame) return arrayCGRect }
その配列を、StoryBoardのインスタンスで GeccoVCに渡します。
let geccoVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Gecco") as! GeccoVC geccoVC.uiLabels_frames = passCGRect()
受け取った座標を元に、スポットライトを生成。
(spotlightView.apperは、最初に表示されるスポットライト)
spotlightView.appear(Spotlight.RoundedRect(center: CGPoint(x: uiLabels_frames[0].midX, y: uiLabels_frames[0].midY), size: CGSize(width: uiLabels_frames[0].width + 40, height: uiLabels_frames[0].height + 40), cornerRadius: 50))
次に表示させるスポットライトを生成。
(spotlightMoveは、次に表示されるスポットライト、指定した座標にスポットライトが移動する)
func spotlightMove(_ cgpoint:CGPoint, _ cgsize:CGSize) { spotlightView.move(Spotlight.RoundedRect(center: cgpoint, size: cgsize, cornerRadius: 50)) }
spotlightMove(CGPoint(x: uiLabels_frames[1].midX, y: uiLabels_frames[1].midY), CGSize(width: uiLabels_frames[1].width + 40, height: uiLabels_frames[1].height + 40))
spotlightMove(CGPoint(x: uiLabels_frames[2].midX, y: uiLabels_frames[2].midY), CGSize(width: uiLabels_frames[2].width + 40, height: uiLabels_frames[2].height + 40))
スポットライトを進行させるためのdelegate
次に、スポットライトのアニメーションの実行/解除、を設定します。
GeccoVCでSpotlightViewControllerDelegateを記述。
extension GeccoVC: SpotlightViewControllerDelegate { //画面が表示される時 func spotlightViewControllerWillPresent(_ viewController: SpotlightViewController, animated: Bool) { } //画面タップ時 func spotlightViewControllerTapped(_ viewController: SpotlightViewController, isInsideSpotlight: Bool) { } //画面が消える時 func spotlightViewControllerWillDismiss(_ viewController: SpotlightViewController, animated: Bool) { spotlightView.disappear() } }
今回は、画面をタップするたびにスポットライトを表示させていくので、下記のメソッドを用意。
(実行されるたびに、updateIndexが加算され、3つのスポットライトが順番に表示され、最後にスポットライトが消える)
(終了後にupdteIndexは初期化されるので、再び実行すればスポットライトが表示される)
func nextSpotlight() { switch updateIndex { case 0: spotlightView.appear(Spotlight.RoundedRect(center: CGPoint(x: uiLabels_frames[0].midX, y: uiLabels_frames[0].midY), size: CGSize(width: uiLabels_frames[0].width + 40, height: uiLabels_frames[0].height + 40), cornerRadius: 50)) case 1: spotlightMove(CGPoint(x: uiLabels_frames[1].midX, y: uiLabels_frames[1].midY), CGSize(width: uiLabels_frames[1].width + 40, height: uiLabels_frames[1].height + 40)) case 2: spotlightMove(CGPoint(x: uiLabels_frames[2].midX, y: uiLabels_frames[2].midY), CGSize(width: uiLabels_frames[2].width + 40, height: uiLabels_frames[2].height + 40)) case 3: dismiss(animated: true, completion: nil) default: break } updateIndex += 1 }
上記のメソッドをdelegateのメソッド内に記述。
func spotlightViewControllerWillPresent(_ viewController: SpotlightViewController, animated: Bool) { nextSpotlight() } //画面タップ時 func spotlightViewControllerTapped(_ viewController: SpotlightViewController, isInsideSpotlight: Bool) { nextSpotlight() }
これで、下記の処理が実装できました。
- GeccoVCが開く→1つ目のスポットライト
- 画面をタップ→2つ目のスポットライト
- 画面をタップ→3つ目のスポットライト
- 画面をタップ→スポットライトのアニメーションが終了
テキストの生成
とりあえず、下記のコードでスポットライトと一緒に表示させるテキストを3つ生成します。
また、各テキストの座標はUIButtonの座標の近くに表示させるので、取得したUIButtonの座標を元にframeを指定します。
var uiLabels = [UIView]() override func viewDidLoad() { super.viewDidLoad() delegate = self //使用するラベルは3つなので、0...2 for index in 0...2 { createLabels(index) } } //メッセージ用のUILabelを生成 func createLabels(_ index:Int) { let label = UILabel() switch index { case 0: label.text = "ボタン1" case 1: label.text = "ボタン2" case 2: label.text = "ボタン3" default: break } //labelの設定 label.textColor = .white label.font = UIFont.systemFont(ofSize: 16) label.textAlignment = .center label.numberOfLines = 0 label.frame = CGRect(x: 0, y: uiLabels_frames[index].origin.y + uiLabels_frames[index].height + 20, width: screenSize.width, height: 60) self.view.addSubview(label) //uiLabelsに格納 uiLabels.append(label) }
テキストのアニメーションを設定
まずは、下記のメソッドを定義。
(viewは格納したUILabel、indexは要素番号)
(後ほど、解説します。)
func updateSpotlightLabel(_ labelAnimated: Bool) { uiLabels.enumerated().forEach { index, view in UIView.animate(withDuration: labelAnimated ? 0.25 : 0) { view.alpha = index == self.updateIndex ? 1 : 0 } } }
スポットライトと同時に表示させるので、上記のメソッドをnextSpotlightの中に記述。
var updateIndex = 0
func nextSpotlight(_ spotAnimated: Bool) {
updateSpotlightLabel(spotAnimated) updateIndex += 1
}
updateSpotlightLabelのメソッドは、
- 生成したUILabelを全て表示
- updateIndexに対応するテキストのみを表示(それ以外は非表示)
の仕組みで動いてます。
つまり、updateIndexの値に対して、
- 0 : button1、label("ボタン1")
- 1 : button2、label("ボタン2")
- 2 : button3、label("ボタン3")
のように対応しており、updateIndexの各値に対応するテキストのみを表示しているのです。
↓
スポットライトとテキストを表示
そして最後は、GeccoVCをpresentで表示させれば、スポットライトのアニメーションが開始されます。
let geccoVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Gecco") as! GeccoVC self.present(geccoVC, animated: true, completion: nil)
結果
ソースコード
「ストーリーボード」
「DisplayGecco」
import UIKit class DisplayGecco: UIViewController { @IBOutlet weak var button1: UIButton! @IBOutlet weak var button2: UIButton! @IBOutlet weak var button3: UIButton! func passCGRect() -> [CGRect] { var arrayCGRect = [CGRect]() arrayCGRect.append(button1.frame) arrayCGRect.append(button2.frame) arrayCGRect.append(button3.frame) return arrayCGRect } @IBAction func start(_ sender: Any) { let geccoVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Gecco") as! GeccoVC geccoVC.alpha = 0.5 //GeccoVCのスポットライトやテキストに必要な座標データを渡す geccoVC.uiLabels_frames = passCGRect() //移行 self.present(geccoVC, animated: true, completion: nil) } }
「GeccoVC」
import UIKit import Gecco class GeccoVC: SpotlightViewController { //UILabelを格納 var uiLabels = [UIView]() //各UILabelの座標データ var uiLabels_frames = [CGRect()] //LabelやSpotlightの表示を決める var updateIndex = 0 //画面サイズ let screenSize = UIScreen.main.bounds.size override func viewDidLoad() { super.viewDidLoad() delegate = self for index in 0...2 { createLabels(index) } } //メッセージ用のUILabelを生成 func createLabels(_ index:Int) { let label = UILabel() switch index { case 0: label.text = "ボタン1" case 1: label.text = "ボタン2" case 2: label.text = "ボタン3" default: break } label.textColor = .white label.font = UIFont.systemFont(ofSize: 16) label.textAlignment = .center label.numberOfLines = 0 label.frame = CGRect(x: 0, y: uiLabels_frames[index].origin.y + uiLabels_frames[index].height + 20, width: screenSize.width, height: 60) self.view.addSubview(label) uiLabels.append(label) } //Spotlightを表示 func nextSpotlight(_ spotAnimated: Bool) { updateSpotlightLabel(spotAnimated) switch updateIndex { case 0: spotlightView.appear(Spotlight.RoundedRect(center: CGPoint(x: uiLabels_frames[0].midX, y: uiLabels_frames[0].midY), size: CGSize(width: uiLabels_frames[0].width + 40, height: uiLabels_frames[0].height + 40), cornerRadius: 50)) case 1: spotlightMove(CGPoint(x: uiLabels_frames[1].midX, y: uiLabels_frames[1].midY), CGSize(width: uiLabels_frames[1].width + 40, height: uiLabels_frames[1].height + 40)) case 2: spotlightMove(CGPoint(x: uiLabels_frames[2].midX, y: uiLabels_frames[2].midY), CGSize(width: uiLabels_frames[2].width + 40, height: uiLabels_frames[2].height + 40)) case 3: dismiss(animated: true, completion: nil) default: break } updateIndex += 1 } //Spotlightを処理 func spotlightMove(_ cgpoint:CGPoint, _ cgsize:CGSize) { spotlightView.move(Spotlight.RoundedRect(center: cgpoint, size: cgsize, cornerRadius: 50)) } //UILabelを表示 func updateSpotlightLabel(_ labelAnimated: Bool) { uiLabels.enumerated().forEach { index, view in UIView.animate(withDuration: labelAnimated ? 0.25 : 0) { view.alpha = index == self.updateIndex ? 1 : 0 } } } } extension GeccoVC: SpotlightViewControllerDelegate { func spotlightViewControllerWillPresent(_ viewController: SpotlightViewController, animated: Bool) { nextSpotlight(false) } func spotlightViewControllerTapped(_ viewController: SpotlightViewController, isInsideSpotlight: Bool) { nextSpotlight(true) } func spotlightViewControllerWillDismiss(_ viewController: SpotlightViewController, animated: Bool) { spotlightView.disappear() } }
参考