データ永続化の方法は色々ありますが、NSUserDefaultの利点はアプリ起動時にデータをメモリに読んでからアクセスするので、高速に読込ができる点にあります。今回は、タイトル画面でステージ情報を読み込んでNSUserDefaultに保存し、次の画面で読み込む動作を作ってみます。
独自クラスのオブジェクトをNSUserDefaultで保存/読込する方法は調べると色々出てくるのですが、独自クラスの中に独自クラスがある入れ子状態の場合や、配列の独自クラスはどうするのか?という情報を探しても的確な答えが見つからなかったので、自分で試してみました。
制作環境
- Xcode7.2.1
- Swift2
- SpriteKit
プロジェクトファイルの説明
新規プロジェクトで、Single Viewプリセットを選びます。
画面1(FirstScene)でStageManagerオブジェクトを作成してNSUserDefaultに保存します。
画面遷移させて画面2(SecondScene)を呼び出し、今度はNSUserDefaultに保存されているStageManagerオブジェクトを読み込みます。画面遷移前に保存、遷移後に読込とする事で、ちゃんと保存/読込ができているか確認できます。

独自クラスの配列に入れた独自クラスをNSUserDefaultで保存する
通常のNSUserDefaultの使い方に加えて、やる事は2つだけです。
- 独自クラスの、NSDataへのアーカイブ/アンアーカイブ処理(NSUserDefaultで保存/読込処理をする場所に書く)
- シリアライズとデシリアライズ(NSObjectの継承とNSCodingプロトコルを適用した独自クラス内に書く)
ポイントは、独自クラスのプロパティに独自クラスがある場合、入れ子になってる独自クラスのプロパティ全てでシリアライズとデシリアライズ処理を実装する事です。
SpriteKitを使う準備
SpriteKitを使うので、デフォルトで最初に読み込まれるViewControllerのviewをSKViewにして、FirstSceneを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import UIKit import SpriteKit class ViewController: UIViewController { override func viewDidLoad() { //viewをSKViewに設定 let skView = self.view as! SKView //SKSceneを表示 let scene = FirstScene(size: CGSize(width:750, height:1334)) skView.presentScene(scene) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } |
独自クラス、StageとStageManagerの作成
個別のステージ情報を管理するクラスを作成します。
独自クラスをNSUserDefaultで保存するには、シリアライズとデシリアライズの処理を実装する必要があります。
まずは、独自クラスにNSObjectを継承させて、NSCodingプロトコルを適用します。
ドットインストール風に言うと、「この2つは、NSUserDefaultの保存/読込に対応させるための決まり文句です!」
このNSCodingプロトコルを唱えると、func encodeWithCoder(aCoder: NSCoder) とrequired init(coder: NSCoder) の2つのメソッドを必ず作らなければいけません。この2つのメソッドで、シリアライズとデシリアライズを実装します。メソッドの中身は、下のソースを参考にしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import Foundation class Stage:NSObject, NSCoding{ //Stage基本情報 var title = String() var info = String() var character_list = [[Int]]() var coin_list = [[Int]]() //NSUserDefaultl シリアライズ処理 func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(title, forKey: "title") aCoder.encodeObject(info, forKey: "info") aCoder.encodeObject(character_list, forKey: "character_list") aCoder.encodeObject(coin_list, forKey: "coin_list") } //NSUserDefaultl デシリアライズ処理 required init(coder: NSCoder) { title = coder.decodeObjectForKey("title") as! String info = coder.decodeObjectForKey("info") as! String character_list = coder.decodeObjectForKey("character_list") as! [[Int]] //多次元配列への型キャストは、この様な書き方をします coin_list = coder.decodeObjectForKey("coin_list") as! [[Int]] } //イニシャライザ override init(){ //テストなので、適当な値で初期化しておきます title = "タイトルですよ" info = "ここに説明文はいります。" character_list = [[1,1,1],[2,2,2],[3,3,3]] coin_list = [[4,4,4],[5,5,5],[6,6,6]] } } |
次に、個別のステージ情報を取得したり、ステージ一覧を管理するクラスを作成します。
この様に、独自クラスの中に独自クラスのプロパティが入っている場合は、両方のクラスでNSObjectの継承とNSCodingプロトコルを適用させなければなりません。片方だけだと、エラーになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import Foundation class StageManager:NSObject, NSCoding{ //Stageリスト var stage_list = [Stage]() //独自クラスの配列 //NSUserDefaultl シリアライズ処理 func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(stage_list, forKey: "stage_list") } //NSUserDefaultl デシリアライズ処理 required init(coder: NSCoder) { stage_list = coder.decodeObjectForKey("stage_list") as! [Stage] } //リストを取得 func getStageList(){ //実際は外部ファイルからステージ情報取得する処理を書きます。 //今回はテストなので、直接Stageオブジェクトを4つほど作成 for i in 0...3{ let stage = Stage() stage_list.append(stage) } } //イニシャライザ override init(){ super.init() self.getStageList() //stage_listの取得 } } |
画面遷移の作成
画面1として表示させるSKSceneを作成します。
ViewControllerから最初に呼ばれるゲームタイトル画面を想定しています。この最初の画面で、サーバーからダウンロードしたJSONファイルからステージ情報を全て読み込んでNSUserDefaultに保存しておく事で、画面が変わったりアプリを終了させたりしてもステージ情報へ高速にアクセスできます。
SpriteKitを使用しているので、各画面はSKSceneで作っています。SpriteKitを使わず、通常のアプリ制作の様にStoryboardで画面遷移を作る場合は、各画面のViewControllerに下記の処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class FirstScene:SKScene{ override func didMoveToView(view: SKView) { //シーン名が分かるように、ラベルを配置 let label = SKLabelNode(text: "画面1 タイトルとか") label.position = CGPointMake(self.frame.size.width*0.5, self.frame.size.height*0.5) label.color = SKColor.whiteColor() self.addChild(label) //StageManagerのインスタンスを作成して、NSDataにアーカイブします let stage_manager = StageManager() let archive:NSData = NSKeyedArchiver.archivedDataWithRootObject(stage_manager) //NSUserDefaultインスタンスを作成 let nsud = NSUserDefaults.standardUserDefaults() //key名「stage_manager」に保存する nsud.setObject(archive as AnyObject, forKey: "stage_manager") nsud.synchronize() } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { //どこでも良いので画面タッチされたら、SecondSceneへ画面遷移させる let skView = self.view as! SKView! let scene = SecondScene(size: self.size) scene.scaleMode = .AspectFit skView.presentScene(scene) } } |
画面遷移後に、保存されたStageManagerを読み込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import Foundation import SpriteKit class SecondScene:SKScene{ override func didMoveToView(view: SKView) { //シーン名が分かるように、ラベルを配置 let label = SKLabelNode(text: "画面2 ステージ選択画面とか") label.position = CGPointMake(self.frame.size.width*0.5, self.frame.size.height*0.5) label.color = SKColor.whiteColor() self.addChild(label) //NSUserDefaultインスタンスを作成 let nsud = NSUserDefaults.standardUserDefaults() //key名「stage_manager」を読み込む if let data = nsud.objectForKey("stage_manager") as? NSData{ //読み込めたら…保存されているNSDataをStageManager型にキャストする if let stage_manager = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? StageManager{ //StageManager型にキャストできたら…1番目のStageオブジェクトの中身を表示してみる print ("ステージタイトル:\(stage_manager.stage_list[0].title)") print ("ステージ説明:\(stage_manager.stage_list[0].info)") print ("キャラクター初期座標:\(stage_manager.stage_list[0].character_list)") print ("コイン初期座標:\(stage_manager.stage_list[0].coin_list)") } } } } |