読者です 読者をやめる 読者になる 読者になる

Swiftで遊ぼう! on Hatena

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

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

2017年4月8日:Swift 3に対応*1

yataiblue.hatenablog.com

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

Notificationの実装はできました。

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

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

KVO

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

非常に便利な機能なのですが以下の問題点があります。

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

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

quesera2.hatenablog.jp

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

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

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

llcc.hatenablog.com

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

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

KVODemoプロジェクト

Xcodeプロジェクトを作ってみます。名前はなんでも構いません。「KVODemo」とします。

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

import Foundation

class ObservedModel: NSObject {
    dynamic var changeableValue = ""
}

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

  • addObserver(observer: NSObject, forKeyPath: String, options: NSKeyValueObservingOptions, context: UnsafeMutableRawPointer?)
  • removeObserver(observer: NSObject, forKeyPath: 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()
        
        print("The default value is \(observedModel.changeableValue).")
        
    }
    // アプリが立ち上がった時にデフォルト値をコンソールに表示させます。
        
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        observedModel.addObserver(self, 
                                forKeyPath: "changeableValue", 
                                   options: [.old, .new], context: nil)
        observedModel.changeableValue = "Changed Value"
        // 監視したいインスタンスに監視側、ここでは自分自身(ViewController
        // に当たる「self」を加えています。監視するプロパティは、
        // ObservedModelのプロパティ(dynamicキーワードが付いている
        // 必要があります))の「changeableValue」です。
        // オプション指定で変化前と後に通知をするようにします。
    }
    
    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の要になる変化を読み取るメソッドです。
   // changeableValueの値が変化すると自動的に呼ばれます!

}

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

f:id:yataiblue:20170408144227j:plain

こででKVOが便利に使えそうです。しかしながらNSObjectを使用しているため今後必ず変更が加わると思います。

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

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