Key-Value観測(KVO)はSwiftで利用できますか?


174

もしそうなら、Objective-CでKey-Value観測を使用するときに他に存在しなかった重要な違いはありますか?


2
Swiftを介してUIKitインターフェースでKVOが使用されていることを示すサンプルプロジェクト:github.com/jameswomack/kvo-in-swift
james_womack

@JanDvorakこのトピックのわかりやすい紹介であるKVOプログラミングガイドを参照してください。
ロブ

1
質問に対する回答ではありませんが、didset()関数を使用してアクションを開始することもできます。
Vincent

を使用すると、Swift4のバグがあることに注意してください.initial。解決策については、こちらをご覧くださいAppleのドキュメントをご覧になることを強くお勧めします。最近更新され、多くの重要な注意事項が記載されています。Robの他の回答
Honey

回答:


107

(新しい情報を追加するために編集):Combineフレームワークを使用すると、KVOを使用するのではなく、望んだことを達成できるかどうかを検討します。

はいといいえ。KVOは、いつものようにNSObjectサブクラスで機能します。NSObjectをサブクラス化しないクラスでは機能しません。Swiftには(現在は少なくとも)独自のネイティブな監視システムはありません。

(他のプロパティをObjCとして公開してKVOが機能するようにする方法については、コメントを参照してください)

完全な例については、Appleのドキュメントを参照してください。


74
Xcode 6ベータ5以降dynamic、任意のSwiftクラスでキーワードを使用して、KVOサポートを有効にすることができます。
2014

7
@fabbさん、こんにちは!明確にするために、dynamicキーワードは、Key-Valueを監視可能にするプロパティに適用されます。
ジェリー

5
dynamicキーワードの説明は、Apple Developer Libraryの「CocoaおよびObjective-CでのSwift使用」セクションにあります。
Imanou Petit

6
@fabbのコメントからはこれが明確ではなかったため、KVOに準拠させたいクラス内のプロパティにdynamicキーワードを使用します(クラス自体のキーワードではありません)。これは私のために働いた!dynamic
Tim Camber、2014年

1
あんまり; 「外部」から新しいdidSetを登録することはできません。コンパイル時にその型の一部である必要があります。
Catfish_Man 2016年

155

KVOはSwiftで使用できますがdynamicNSObjectサブクラスのプロパティでのみ使用できます。クラスのbarプロパティを監視したいと考えてくださいFoo。Swift 4では、bardynamicNSObjectサブクラスのプロパティとして次のようします。

class Foo: NSObject {
    @objc dynamic var bar = 0
}

その後、barプロパティの変更を監視するために登録できます。Swift 4およびSwift 3.2では、Swiftでのキー値監視の使用で概説されているように、これは大幅に簡素化されています。

class MyObject {
    private var token: NSKeyValueObservation

    var objectToObserve = Foo()

    init() {
        token = objectToObserve.observe(\.bar) { [weak self] object, change in  // the `[weak self]` is to avoid strong reference cycle; obviously, if you don't reference `self` in the closure, then `[weak self]` is not needed
            print("bar property is now \(object.bar)")
        }
    }
}

Swift 4では、バックスラッシュ文字を使用し\.barてキーパスを強力に入力できるようになりました(これは、bar対象のオブジェクトプロパティの)。また、完了クロージャーパターンを使用しているため、オブザーバーを手動で削除する必要はありません(tokenスコープから外れると、オブザーバーが削除されます)。またsuper、キーがそうでない場合でも、実装の呼び出しについて心配する必要はありません。一致。この特定のオブザーバーが呼び出されたときにのみ、クロージャーが呼び出されます。詳細については、WWDC 2017のビデオ、What's New in Foundationを参照してください。

Swift 3では、これを観察するために少し複雑ですが、Objective-Cで行うことと非常によく似ています。つまり、次のいずれかを実装しますobserveValue(forKeyPath keyPath:, of object:, change:, context:)。(a)コンテキストを処理していることを確認します(superインスタンスが監視するために登録したものではありません)。(b)super必要に応じて、それを処理するか、実装に渡します。そして、必要に応じて、オブザーバーから自分自身を削除するようにしてください。たとえば、割り当てが解除されたときにオブザーバーを削除できます。

Swift 3の場合:

class MyObject: NSObject {
    private var observerContext = 0

    var objectToObserve = Foo()

    override init() {
        super.init()

        objectToObserve.addObserver(self, forKeyPath: #keyPath(Foo.bar), options: [.new, .old], context: &observerContext)
    }

    deinit {
        objectToObserve.removeObserver(self, forKeyPath: #keyPath(Foo.bar), context: &observerContext)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &observerContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }

        // do something upon notification of the observed object

        print("\(keyPath): \(change?[.newKey])")
    }

}

注:Objective-Cで表現できるプロパティのみを監視できます。したがって、ジェネリック、Swift structタイプ、Swift enumタイプなどを監視することはできません。

Swift 2実装の説明については、以下の元の回答を参照してください。


dynamicキーワードを使用してNSObjectサブクラスでKVOを実現する方法については、「CocoaおよびObjective-CでのSwift使用」の「Cocoa設計規則採用」の章の「キー値の監視」セクションを参照してください。ガイドください。

キー値監視は、他のオブジェクトの指定されたプロパティへの変更をオブジェクトに通知できるようにするメカニズムです。クラスがクラスから継承する限り、Swiftクラスでキー値監視を使用できますNSObject。これらの3つのステップを使用して、Swiftでキー値監視を実装できます。

  1. dynamic監視する任意のプロパティに修飾子を追加します。の詳細についてはdynamic動的ディスパッチの要求を参照してください。

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. グローバルコンテキスト変数を作成します。

    private var myContext = 0
  3. キーパスのオブザーバーを追加し、observeValueForKeyPath:ofObject:change:context:メソッドをオーバーライドして、でオブザーバーを削除しdeinitます。

    class MyObserver: NSObject {
        var objectToObserve = MyObjectToObserve()
        override init() {
            super.init()
            objectToObserve.addObserver(self, forKeyPath: "myDate", options: .New, context: &myContext)
        }
    
        override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
            if context == &myContext {
                if let newValue = change?[NSKeyValueChangeNewKey] {
                    print("Date changed: \(newValue)")
                }
            } else {
                super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
            }
        }
    
        deinit {
            objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
        }
    }

[注:このKVOの議論は、Swift 3向けに適合された「CocoaおよびObjective-CでのSwift使用」ガイドから削除されましたが、この回答の冒頭で概説されているとおりに機能します。]


Swiftには独自のネイティブプロパティオブザーバーシステムがあることは注目に値しますが、それは、独自のプロパティの監視時に実行される独自のコードを指定するクラス用です。一方、KVOは、他のクラスの動的プロパティの変更を監視するように登録するように設計されています。


myContext複数のプロパティの目的は何ですか?またどのように観察しますか?
devth 2014

1
KVOプログラミングガイドによると、「オブザーバーとしてオブジェクトを登録するときに、contextポインターを提供することもできます。contextポインターは、observeValueForKeyPath:ofObject:change:context:呼び出されたときにオブザーバーに提供されます。contextポインターは、Cポインターまたはオブジェクト参照にcontextすることができます。監視されている変更を判別するため、またはオブザーバーに他のデータを提供するための一意の識別子として使用されます。」
ロブ

deinitでオブザーバーを削除する必要があります
ジャッキー

3
@devth、私が理解しているように、サブクラスまたはスーパークラスが同じ変数のKVOオブザーバーも登録する場合、observeValueForKeyPathが複数回呼び出されます。この状況では、コンテキストを使用して自分の通知を区別できます。これについての詳細:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey

1
options空のままにした場合、それは単ににchange古い値または新しい値が含まれないことを意味します(たとえば、オブジェクト自体を参照することによって、新しい値を自分で取得する場合があります)。だけを指定し.new、を指定しない場合は、新しい値のみが含まれ、古い値は含まれない.oldことを意味しchangeます(たとえば、多くの場合、古い値が何であるかは気にせず、新しい値のみが気になります)。observeValueForKeyPath古い値と新しい値の両方を渡す必要がある場合は、を指定します[.new, .old]。一番下の行は、options何がchange辞書に含まれるかを指定するだけです。
Rob

92

はいといいえの両方:

  • はい、Swiftで同じ古いKVO APIを使用してObjective-Cオブジェクトを監視できます。から継承するSwiftオブジェクトのプロパティを
    確認することもできdynamicますNSObject
    しかし... いいえ、強く型付けされていません。Swiftネイティブの観測システムがそうであると期待できるからです。
    CocoaおよびObjective-CでのSwiftの使用| キーバリュー観察

  • いいえ、現在、任意のSwiftオブジェクトの組み込みの値監視システムはありません

  • はい、組み込みのプロパティオブザーバーがあり、厳密に型指定されています。
    しかし... いいえ、オブジェクトのプロパティの監視のみを許可し、ネストされた監視(「キーパス」)をサポートしていないため、KVOではありません。明示的に実装する必要があります。
    Swiftプログラミング言語| プロパティオブザーバー

  • はい、明示的に型指定された明示的な値監視を実装し、他のオブジェクトから複数のハンドラーを追加でき、ネスト/「キーパス」もサポートできます。
    しかし... いいえ、監視可能として実装したプロパティに対してのみ機能するため、KVOにはなりません
    このような値の監視を実装するためのライブラリは、次の場所にあります
    。Observable-Swift-KVO for Swift-値の監視とイベント


10

ここで例が少し役立つかもしれません。属性を持つmodelクラスのインスタンスがあり、これらの属性を次のように観察できる場合:Modelnamestate

let options = NSKeyValueObservingOptions([.New, .Old, .Initial, .Prior])

model.addObserver(self, forKeyPath: "name", options: options, context: nil)
model.addObserver(self, forKeyPath: "state", options: options, context: nil)

これらのプロパティを変更すると、次の呼び出しがトリガーされます。

override func observeValueForKeyPath(keyPath: String!,
    ofObject object: AnyObject!,
    change: NSDictionary!,
    context: CMutableVoidPointer) {

        println("CHANGE OBSERVED: \(change)")
}

2
私が間違っていないのであれば、observeValueForKeyPath呼び出しアプローチはSwift2用です。
Fattie

9

はい。

KVOには動的なディスパッチが必要なのでdynamic、メソッド、プロパティ、添え字、または初期化子に修飾子を追加するだけです。

dynamic var foo = 0

dynamic宣言への参照が動的を通じて派遣してアクセスされることを修飾性を保証objc_msgSend


7

ロブの答えに加えて。そのクラスはNSObject、プロパティの変更をトリガーするには3つの方法があります

setValue(value: AnyObject?, forKey key: String)から使用NSKeyValueCoding

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        setValue(NSDate(), forKey: "myDate")
    }
}

使用willChangeValueForKeyしてdidChangeValueForKeyからNSKeyValueObserving

class MyObjectToObserve: NSObject {
    var myDate = NSDate()
    func updateDate() {
        willChangeValueForKey("myDate")
        myDate = NSDate()
        didChangeValueForKey("myDate")
    }
}

を使用しdynamicます。Swiftタイプの互換性を参照してください

メソッドの実装を動的に置き換えるキー値監視などのAPIを使用している場合は、動的修飾子を使用して、Objective-Cランタイムを通じてメンバーへのアクセスを動的にディスパッチすることを要求できます。

class MyObjectToObserve: NSObject {
    dynamic var myDate = NSDate()
    func updateDate() {
        myDate = NSDate()
    }
}

また、プロパティのゲッターとセッターは使用時に呼び出されます。KVOで作業するときに確認できます。これは計算されたプロパティの例です

class MyObjectToObserve: NSObject {
    var backing: NSDate = NSDate()
    dynamic var myDate: NSDate {
        set {
            print("setter is called")
            backing = newValue
        }
        get {
            print("getter is called")
            return backing
        }
    }
}

5

概観

またはを使用しCombineなくても使用NSObjectできますObjective-C

可用性: iOS 13.0+macOS 10.15+tvOS 13.0+watchOS 6.0+Mac Catalyst 13.0+Xcode 11.0+

注:値型ではなくクラスでのみ使用する必要があります。

コード:

Swiftバージョン:5.1.2

import Combine //Combine Framework

//Needs to be a class doesn't work with struct and other value types
class Car {

    @Published var price : Int = 10
}

let car = Car()

//Option 1: Automatically Subscribes to the publisher

let cancellable1 = car.$price.sink {
    print("Option 1: value changed to \($0)")
}

//Option 2: Manually Subscribe to the publisher
//Using this option multiple subscribers can subscribe to the same publisher

let publisher = car.$price

let subscriber2 : Subscribers.Sink<Int, Never>

subscriber2 = Subscribers.Sink(receiveCompletion: { print("completion \($0)")}) {
    print("Option 2: value changed to \($0)")
}

publisher.subscribe(subscriber2)

//Assign a new value

car.price = 20

出力:

Option 1: value changed to 10
Option 2: value changed to 10
Option 1: value changed to 20
Option 2: value changed to 20

参照:


4

現在、Swiftは 'self'以外のオブジェクトのプロパティ変更を監視するための組み込みメカニズムをサポートしていないため、KVOはサポートされていません。

ただし、KVOはObjective-CおよびCocoaの非常に重要な部分であるため、将来的に追加される可能性が非常に高いようです。現在のドキュメントはこれを暗示しているようです:

Key-Value監視

今後の情報。

CocoaおよびObjective-CでのSwiftの使用


2
明らかに、あなたが参照しているそのガイドは、SwiftでKVOを実行する方法を説明しています。
ロブ

4
はい、2014年9月に実装されました
Max MacLeod

4

Xcode7ベータ版に更新した後、「メソッドはスーパークラスのメソッドをオーバーライドしません」というメッセージが表示される可能性があることに注意してください 。これは、引数のオプション性によるものです。監視ハンドラが次のようになっていることを確認してください。

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)

2
Xcodeベータ6では、次のものが必要です:func observeValueForKeyPath(keyPath:String ?, ofObject object:AnyObject ?, change:[String:AnyObject] ?, context:UnsafeMutablePointer <Void>)
hcanfly

4

これは少数の人々に役立つかもしれません-

// MARK: - KVO

var observedPaths: [String] = []

func observeKVO(keyPath: String) {
    observedPaths.append(keyPath)
    addObserver(self, forKeyPath: keyPath, options: [.old, .new], context: nil)
}

func unObserveKVO(keyPath: String) {
    if let index = observedPaths.index(of: keyPath) {
        observedPaths.remove(at: index)
    }
    removeObserver(self, forKeyPath: keyPath)
}

func unObserveAllKVO() {
    for keyPath in observedPaths {
        removeObserver(self, forKeyPath: keyPath)
    }
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let keyPath = keyPath {
        switch keyPath {
        case #keyPath(camera.iso):
            slider.value = camera.iso
        default:
            break
        }
    }
}

私はSwift 3でこの方法でKVOを使用していました。このコードは、ほとんど変更することなく使用できます。


1

Intなどの型で問題が発生した場合のもう1つの例は?そしてCGFloat?。クラスをNSObjectのサブクラスとして設定し、変数を次のように宣言するだけです。

class Theme : NSObject{

   dynamic var min_images : Int = 0
   dynamic var moreTextSize : CGFloat = 0.0

   func myMethod(){
       self.setValue(value, forKey: "\(min_images)")
   }

}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.