いものやま。

雑多な知識の寄せ集め

「BirdHead」の仕上げをしてみた。(その5)

昨日はゲーム結果の表示を実装した。

これで残すはゲームの中断と再開のみ。
まずはモデルの保存と復元を実装していく。

GameInfoの修正

基本的にはYWFと同じ。
以下が参考になると思う。

同様に、GameInfoを修正して、保存と復元が出来るようにする。

//==============================
// BirdHead
//------------------------------
// GameInfo.swift
//==============================

import Foundation

// 省略

class GameInfo: NSObject {
  // 省略
  
  // for save and load
  private static let saveDataFileName: String = "gameinfo.plist"
  private static var saveDataURL: NSURL = {
    let fileManager = NSFileManager.defaultManager()
    let supportDirectory = try! fileManager.URLForDirectory(.ApplicationSupportDirectory,
                                                            inDomain: NSSearchPathDomainMask.UserDomainMask,
                                                            appropriateForURL: nil,
                                                            create: true)
    return supportDirectory.URLByAppendingPathComponent(GameInfo.saveDataFileName)
  }()
  static let saveGameInfoKey: String = "com.ouka-do.BirdHead.saveGameInfo"
  static let deleteGameInfoKey: String = "com.ouka-do.BirdHead.deleteGameInfo"
  
  class func fromSaveData() -> GameInfo? {
    if let savedData = NSDictionary(contentsOfURL: GameInfo.saveDataURL) {
      let deck = Deck()
      let playerCount = savedData["playerCount"] as! Int
      
      let gameInfo = GameInfo(deck: deck, playerCount: playerCount)
      
      gameInfo.inDeal = savedData["inDeal"] as! Bool
      gameInfo.playerHands = savedData["playerHands"] as! [[Int]]
      gameInfo.trickCount = savedData["trickCount"] as! Int
      gameInfo.turnPlayerIndex = savedData["turnPlayerIndex"] as! Int
      gameInfo.minusPointCards = savedData["minusPointCards"] as! [[Int]]
      gameInfo.isEnd = savedData["isEnd"] as! Bool
      
      let usedCardCount = savedData["usedCardCount"] as! Array<Dictionary<String, Int>>
      for data in usedCardCount {
        let card = data["card"]!
        let count = data["count"]!
        gameInfo.usedCardCount[card] = count
      }
      
      let actionsInTrick = savedData["actionsInTrick"] as! Array<Dictionary<String, AnyObject>>
      for data in actionsInTrick {
        let action = data["action"] as! String
        let cards = data["cards"] as! [Int]
        if action == "Play" {
          gameInfo.actionsInTrick.append(Action.play(cards))
        } else {
          gameInfo.actionsInTrick.append(Action.discard(cards))
        }
      }
      
      let removedCardCount = savedData["removedCardCount"] as! Array<Dictionary<String, Int>>
      for data in removedCardCount {
        let card = data["card"]!
        let count = data["count"]!
        for _ in 0..<count {
          try! gameInfo.deck.removeCard(card)
        }
      }
      
      return gameInfo
    } else {
      return nil
    }
  }

  // 省略

  private var deck: Deck
  private(set) var playerCount: Int

  private(set) var inDeal: Bool
  private var playerHands: [[Int]]

  private(set) var trickCount: Int
  private(set) var turnPlayerIndex: Int
  private(set) var actionsInTrick: [Action]

  private(set) var usedCardCount: [Int: Int]
  private(set) var minusPointCards: [[Int]]

  private(set) var isEnd: Bool

  private var observers: [ObjectIdentifier: GameInfoObserver]
  
  private var saveEventObserver: NSObjectProtocol!
  private var deleteEventObserver: NSObjectProtocol!
  
  init(deck: Deck, playerCount: Int, startPlayerIndex: Int = 0) {
    // 省略
    
    self.saveEventObserver = nil
    self.deleteEventObserver = nil
    
    super.init()
    
    let notificationCenter = NSNotificationCenter.defaultCenter()
    self.saveEventObserver = notificationCenter.addObserverForName(GameInfo.saveGameInfoKey,
                                                                   object: nil,
                                                                   queue: nil) { notification in
      self.save()
    }
    self.deleteEventObserver = notificationCenter.addObserverForName(GameInfo.deleteGameInfoKey,
                                                                     object: nil,
                                                                     queue: nil) { notification in
      self.delete()
    }
  }

  // 省略
  
  func save() {
    var saveData = [String: AnyObject]()
    
    saveData["playerCount"] = self.playerCount
    saveData["inDeal"] = self.inDeal
    saveData["playerHands"] = self.playerHands
    saveData["trickCount"] = self.trickCount
    saveData["turnPlayerIndex"] = self.turnPlayerIndex
    saveData["minusPointCards"] = self.minusPointCards
    saveData["isEnd"] = self.isEnd

    // Dictionary<Int, Int> cannot be saved!
    // convert it into Array<Dictionary<String, Int>>.
    var usedCardCount = Array<Dictionary<String, Int>>()
    for (card, count) in self.usedCardCount {
      usedCardCount.append(["card": card, "count": count])
    }
    saveData["usedCardCount"] = usedCardCount
    
    // Array<Action> cannot be saved.
    // convert it into Array<Dictionary<String, AnyObject>>.
    var actionsInTrick = Array<Dictionary<String, AnyObject>>()
    for action in self.actionsInTrick {
      switch action {
      case let .Play(cards):
        actionsInTrick.append(["action": "Play", "cards": cards])
      case let .Discard(cards):
        actionsInTrick.append(["action": "Discard", "cards": cards])
      }
    }
    saveData["actionsInTrick"] = actionsInTrick
    
    // deck info
    var removedCardCount = Array<Dictionary<String, Int>>()
    for (card, count) in self.deck.removedCardCount {
      removedCardCount.append(["card": card, "count": count])
    }
    saveData["removedCardCount"] = removedCardCount
    
    (saveData as NSDictionary).writeToURL(GameInfo.saveDataURL, atomically: true)
  }
  
  func delete() {
    let fileManager = NSFileManager.defaultManager()
    _ = try? fileManager.removeItemAtURL(GameInfo.saveDataURL)
    
    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.removeObserver(self.saveEventObserver,
                                      name: GameInfo.saveGameInfoKey,
                                      object: nil)
    notificationCenter.removeObserver(self.deleteEventObserver,
                                      name: GameInfo.deleteGameInfoKey,
                                      object: nil)
  }
  
  // 省略
}

やっている内容は、保存の方では各プロパティを辞書に格納してそれをplistファイルに書き出し、復元の方では逆にplistファイルから辞書を読みだして各プロパティにセットする、というもの。

ちょっとハマったこととして、Swiftの辞書でキーがIntのものをplistファイルに書き出そうとすると、アプリがクラッシュするという問題。
Swiftの世界では全部オブジェクトなんだけど、Objective-Cの世界では(Cの世界なので)整数とオブジェクト(=ポインタ)は別というのがおそらく原因で、辞書(Dictionary)のキーがオブジェクトでない場合、Objective-Cの辞書(NSDictionary)にうまく変換できず、問題が発生するのだと思う。
(これ自体はおそらくSwiftのバグ)

そこで、Dictionary<Int, Int>を無理やりArray<Dictionary<String, Int>>に変換して保存し、復元するときもその逆を行うということをしている。

保存の通知

モデルの保存が出来るようになったら、必要なタイミングで通知が来るようにする。

以下を参照。

AppDelegateを次のように修正。

//==============================
// BirdHead
//------------------------------
// AppDelegate.swift
//==============================

import UIKit

@UIApplicationMain
class AppDelegate: NSObject, UIApplicationDelegate {
  var window: UIWindow?
  
  func applicationWillResignActive(application: UIApplication) {
    NSNotificationCenter.defaultCenter().postNotificationName(GameInfo.saveGameInfoKey, object: nil)
  }
  
  func applicationWillTerminate(application: UIApplication) {
    NSNotificationCenter.defaultCenter().postNotificationName(GameInfo.saveGameInfoKey, object: nil)
  }
}

これで、アプリが切り替わろうとしたり、終了させられるタイミングで、保存の通知が来て、モデルを保存できるようになった。

今日はここまで!