Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 522 - KVO(Key-Value Observing)

2018年6月15日:Swift 4.2に対応させました。KVOはあまり進化してません(^_^;)
2017年4月8日:Swift 3に対応*1

yataiblue.hatenablog.com

デザイン・パターンのオブザーバー・パターンの勉強をしているところです。

Notificationの実装はできました。

しかし、もう一つのデザインパターン、KVOのコーディングが上手くいきません。

Objective-Cランタイム環境での利用を前提としていて、Swiftで実装されていないため使用法に制限があるようです。

KVO

KVOとは、「キー値監視(Key-Value Observing)」システムの事で、オブジェクトプロパティの変化を監視して、変化すれば特定のメソッドを発動する便利な機能です。MVCモデルをみれば、M(モデル)のプロパティが変化すると、その変更を捉えて即座にViewに反映させる使用法が一般的です。

非常に便利な機能なのですが以下の問題点があります(iOS12でも同様です)。

  • Objective-Cランタイムが必須(Swiftでサポートされていない)なため、NSObjectを継承していないと機能しません。監視するクラスも監視されるクラスもNSObjectを継承する必要があります。
  • 監視できるプロパティは、StringやArrayなどNSObjectに互換性のあるものだけに限られます*2
  • SwiftでObjective-Cの利用をすることを明示する必要があるので、監視したいプロパティに「@objc」と「dynamic」キーワードをつける必要があります。

ということは、KVOの実装は、Swiftスタイルじゃないってことになります。私はObjective-Cのコードは消えていくと思っているのですが、ここにもそんな内容の話がありました*3

quesera2.hatenablog.jp

Swiftのオープンソース化はそこから言語コミュニティが活発化してSwift 3へ進化することで結実するって感じなのですかねー。
そのときSwift 2.Xの資産は全てゴミになると思われますが*4…。

最後の言葉を読むと、今やっていることがむなしく思えますが、Swiftの勉強を始める時から覚悟していた事です。次々と新しくなる変化って嬉しいものです。なんとかついて行きたいと思っています。

KVOを理解するために、ネットを探しているとありました!

llcc.hatenablog.com

しめ鯖さんの説明が理解できたので嬉しかったのですが、最初はコードが動きませんでした。

UIViewControllerがNSObjectを継承していると思っていなかったんで、「observeValue()」メソッドは使えないと思い込んでい真下が、質問したところ、パラメーターの書き方に変更があっただけでした(^_^;)

KVODemoプロジェクト

Xcodeプロジェクトを作ってみます。名前はなんでも構いませんが「KVODemo」とします。このプロジェクトの目的を簡単に説明すると

  1. ObservedModelという監視したいカスタムクラスを作って、String型のプロパティを1つだけ保持
  2. ObeservedModelが持つプロパティの変化を捉えるオブザーバーにViewControllerクラスを設定
  3. ViewControllerクラスのライフサイクルを利用してObservedModelのプロパティを変更して挙動を確認

このプロジェクトに新しい「ObservedModel」と名前の付けたSwiftファイルを加え、NSObjectクラスを継承した「ObservedModel」クラスを次のように作ります。

import Foundation

class ObservedModel: NSObject {
    @objc dynamic var changeableValue = "Default Value"
}

これでNSObjectの持っているKVOに関連する以下のメソッドを使えるようになります。

  • addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)
  • removeObserver(_ observer: NSObject, forKeyPath keyPath: String)
  • observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

上記のメソッドを使ってObservedModelクラスインスタンスをViewControllerで監視させます。

ObservedModelクラスの持つchangeableValueのデフォルト値は「Default Value」なんで、アプリが立ち上がった時(viewDidLoad)はそのままです。次に画面が描出される時(viewWillAppear)時に「Changed Value」に変更します。

すると、その変化が監視されているので、observeValueメソッドが自動的に発動される仕組みです。

class ViewController: UIViewController {
    
    let observedModel = ObservedModel()
    // 監視したいインスタンスをプロパティとして持ちます。

    override func viewDidLoad() {
        super.viewDidLoad()

        observedModel.addObserver(self, 
                           forKeyPath: "changeableValue", 
                              options: [.old, .new], context: nil)
        // 自分自身(self)をオブサーバーとして設定してchangeableValueを監視
        // 変化する前の値と変化後の値を保持します
       print("The default value is \(observedModel.changeableValue).")
        // 変化前のデフォルト値をコンソールに表示させます。
    }
    // アプリが立ちあがった時にまず呼ばれるviewDidLoad()メソッドで
    // オブサーバーを設定します
        
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        observedModel.changeableValue = "Changed Value"
        // 画面が表示される直前に呼ばれるviewWillAppear()メソッドで
        // 監視されているプロパティchangeabeValueを変更する
    }
    
    override func viewWillDisappear(_ animated: Bool) {
       super.viewWillDisappear(animated)
       observedModel.removeObserver(self, forKeyPath: "changeableValue")
       // メモリーリークを回避するために必ずremoveも必要です。
       // このステップはNotificationと同じです。
    }
    
    override func observeValue(forKeyPath keyPath: String?, 
                            of object: Any?, 
                               change: [NSKeyValueChangeKey : Any]?, 
                              context: UnsafeMutableRawPointer?) {
        
        guard let key = keyPath else {
            return print("error")
        }
        
        guard let value = object as? ObservedModel else {
            return print("error2")
        }
        
        print("\"\(key)\" was changed to \(value.changeableValue).")
    }
   // これがKVOの要です。監視されているプロパティの変更があれば
   // 自動的に発動されます

}

ランすすると問題無く動きます。

f:id:yataiblue:20170408144227j:plain

こででKVOが便利に使えそうです。しかしながらNSObjectを使用しているため今後必ず変更が加わると記事を書いた当初は考えていましたがが、Swift 4 時代になっても上記の使い方に大きな変更は加わっていません。それでも、使えるメソッドに拡張がありKVOで使えるメソッド「observe()」で、クロージャー関数を扱えるようになったのが大きいですね。

  • observe(_ keyPath: KeyPath, options: NSKeyValueObservingOptions = default, changeHandler: @escaping (ObservedModel, NSKeyValueObservedChange) -> Void) -> NSKeyValueObservation

これだけの説明でいきなり使える人はプログラミング上級者ですね。私は少し試行錯誤を繰り返しました。NSKeyValueObservationクラスをオブザーバーとして使います。ということで上記のデモのコードをSwift 4風に書き換えてみましょう。ObservedModelクラスに変更はありません。ViewControllerクラスを次のように変更するんです。

let observedModel = ObservedModel()
var keyValueObservations = [NSKeyValueObservation]()

override func viewDidLoad() {
  super.viewDidLoad()

  print("The default value is \(observedModel.changeableValue)")
  let keyValueObservation = 
    observedModel.observe(\.changeableValue, options: [.new, .old]) {
      (_, change) in
      print("\"The value\" changed to \(change.newValue!)")
  }
  keyValueObservations.append(keyValueObservation)

}

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
                              
  observedModel.changeableValue = "Changed Value"
}

どうです!同じ結果が得られたでしょ。[NSKeyValueObservation]とアレー型にしているのは監視する項目が増えた時用ですね。クロージャーの記述法が分からなければ、Swiftで遊ぼう! - 362 - Swift 3.0 : 関数(Functions) - Swiftで遊ぼう! on Hatenaを見てください。あれ?Swift 3向けの記事だ... Swift 4で変更あったかな?また確認しなければならない事項が増えました。

*1:2017年3月25日:Xcode 8で動くことを確認しました。

*2:structやenamuなどで使えません

*3:この記事を書いた頃はSwift 2で使われていました。Objective-Cのコードは未だに消えて無くなっていません

*4:確かにSwift 4になるとSwift 2のコードはゴミかな?