いものやま。

雑多な知識の寄せ集め

変種オセロの仕上げをしてみた。(その6)

昨日はHistoryクラスを実装し、棋譜の保存が出来るようにした。

今日は、ライフサイクルのイベントに合わせて、棋譜の保存を実行できるようにしていく。

UIApplicationDelegateプロトコル

iOSでは、アプリの状態が切り替わるタイミングに呼ばれるメソッドがUIApplicationDelegateプロトコルで用意されていて、これに準拠したクラスをUIApplicationの委譲先にしてやることで、各タイミングでそれらのメソッドが呼ばれるようになっている。
(なお、クラスを委譲先にするには、@UIApplicationMain属性をつければいい)
なので、アプリの状態が切り替わるタイミングで何か処理を行いたい場合、UIApplicationDelegateプロトコルに準拠したクラスで、必要なメソッドを用意すればいい。

今回、棋譜の保存を行いたいのは、アプリがバックグラウンドになろうとするタイミングや、あるいは、アプリが終了させられるタイミングなので、UIApplicationDelegate#applicationWillResignActive(_: UIApplication)、および、UIApplicationDelegate#applicationWillTerminate(_: UIApplication)で、棋譜の保存が出来るようにする。

通知による保存

さて、上記のメソッドで棋譜を保存したいんだけど、困るのがどうやって保存したいHistoryへの参照を得るのか、ということ。
というのも、Historyを保持しているのはBoardNodeで、そのBoardNodeを保持しているのはGameSceneで、そのGameSceneを表示しているのはSKViewで、SKViewを保持しているのはViewControllerで、というふうに、普通にViewの階層を辿っていくのだと、けっこう深くなる。
それに、BoardNodeはHistoryを保持しているけれど、外部には公開をしていないので、BoardNodeからHistoryを保存させようとすると、BoardNodeに新たにインタフェースを追加しないといけなくなる。
(まぁ、すでに"deleteHistory()"というインタフェースを追加しているのだけど・・・)

そこで、NSNotificationCenterを使って、Historyでは保存のタイミングを通知で教えてもらうように準備しておいて、UIApplicationDelegateに準拠したクラスからは、その通知を行うことでHistoryに保存をさせる、という方法を使うことにした。
これなら、UIApplicationDelegateに準拠したクラスからHistoryへの参照を知る必要はなく、必要なのは、Historyを保存させるための通知のキーだけになる。

まず、Historyを次のように修正。

//==============================
// YWF
//------------------------------
// History.swift
//==============================

import Foundation

public class History: NSObject {
  // 省略

  public static let saveHistoryKey: String = "saveHistory"

  // 省略  
  
  private var eventObserver: NSObjectProtocol!

  public override init() {
    // 省略

    self.eventObserver = nil
    
    super.init()
    
    let notificationCenter = NSNotificationCenter.defaultCenter()
    self.eventObserver = notificationCenter.addObserverForName(History.saveHistoryKey,
                                                               object: nil,
                                                               queue: nil) {
      (notification: NSNotification!) -> Void in
      self.save()
    }
  }
  
  // 省略
  
  public func delete() {
    // 省略
    
    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.removeObserver(self.eventObserver,
                                      name: History.saveHistoryKey,
                                      object: nil)
  }
}

ちょっと注意しないといけないのが、NSNotificationCenter#addObserverForName(_: String?, object: AnyObject?, queue: NSOperationQueue?, usingBloc: (NSNotification!) -> Void)は、戻り値としてオブザーバをラップしたオブジェクトを返すということ。
NSNotificationCenter#removeObserver(_: AnyObject, name: String?, object: AnyObject?)の第一引数では、この戻り値のオブジェクトを指定してやる必要がある。
(最初、selfを指定していたんだけど、通知が来なくならないので困った。よくよく考えると、オブザーバとして登録されているのはブロックオブジェクトであってHistory自身ではない! なので、戻り値のオブジェクトの方を指定してやらないといけない)

そして、UIApplicationDelegateに準拠したAppDelegateクラスでは、アプリがバックグラウンドになろうとするタイミング、および、アプリが終了させられるタイミングで、通知を投げるように修正。
具体的には、以下のとおり。

//==============================
// YWF
//------------------------------
// AppDelegate.swift
//==============================

import UIKit

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

これで、意図したタイミグでHistoryに保存をしろという通知が行くようになる。

動作確認

まず、通知による保存を行っていない状態だと、アプリを終了して再度立ち上げると、スタート画面に戻ってしまっているのが分かると思う。

一方、通知による保存を行うようにすると、アプリを終了して再度立ち上げたときにも、ちゃんとゲームが再開されているのが分かると思う。
それに、ちゃんと再開後もundoを行ったりもできるし、undoを行った後に再度終了→起動をしても、ちゃんと動作している。
あと、ゲームを終了させてからアプリを終了→起動の場合には、ちゃんとスタート画面から始まるようになっている。

今日はここまで!