Apakah observasi nilai kunci (KVO) tersedia di Swift?

174

Jika demikian, adakah perbedaan utama yang tidak ada ketika menggunakan observasi nilai kunci di Objective-C?

codeperson
sumber
2
Contoh proyek yang menunjukkan KVO digunakan dalam antarmuka UIKit melalui Swift: github.com/jameswomack/kvo-in-swift
james_womack
@JanDvorak Lihat Panduan Pemrograman KVO , yang merupakan pengantar yang bagus untuk topik ini.
Rob
1
Meskipun bukan jawaban untuk pertanyaan Anda, Anda juga dapat memulai tindakan menggunakan fungsi didset ().
Vincent
Perhatikan ada bug Swift4 saat Anda menggunakan .initial. Untuk solusinya lihat di sini . Saya sangat merekomendasikan untuk melihat dokumen Apple . Sudah diperbarui baru-baru ini dan mencakup banyak catatan penting. Lihat juga jawaban
Honey

Jawaban:

107

(Diedit untuk menambahkan info baru): pertimbangkan apakah menggunakan kerangka kerja Combine dapat membantu Anda mencapai apa yang Anda inginkan, daripada menggunakan KVO

Iya dan tidak. KVO bekerja pada subclass NSObject seperti biasanya. Ini tidak berfungsi untuk kelas yang tidak mensubk NSObject. Swift tidak (saat ini setidaknya) memiliki sistem pengamatan asli sendiri.

(Lihat komentar untuk cara mengekspos properti lain sebagai ObjC sehingga KVO bekerja pada mereka)

Lihat Dokumentasi Apple untuk contoh lengkap.

Catfish_Man
sumber
74
Karena Xcode 6 beta 5 Anda dapat menggunakan dynamickata kunci pada kelas Swift apa saja untuk mengaktifkan dukungan KVO.
Fabb
7
Hore untuk @fabb! Untuk kejelasan, dynamickata kunci berada di properti yang Anda ingin membuat nilai kunci dapat diamati.
Jerry
5
Penjelasan untuk dynamickata kunci dapat ditemukan di Menggunakan Perpustakaan Swift Apple Library dengan bagian Kakao dan Objective-C .
Imanou Petit
6
Karena ini tidak jelas bagi saya dari komentar @ fabb: gunakan dynamickata kunci untuk properti apa pun di dalam kelas yang ingin Anda patuhi KVO (bukan dynamickata kunci pada kelas itu sendiri). Ini berhasil untuk saya!
Tim Camber
1
Tidak juga; Anda tidak dapat mendaftarkan didSet baru dari "luar", itu harus menjadi bagian dari tipe itu pada waktu kompilasi.
Catfish_Man
155

Anda dapat menggunakan KVO di Swift, tetapi hanya untuk dynamicproperti NSObjectsubclass. Pertimbangkan bahwa Anda ingin mengamati barproperti Fookelas. Di Swift 4, tentukan barsebagai dynamicproperti di NSObjectsubkelas Anda :

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

Anda kemudian dapat mendaftar untuk mengamati perubahan pada barproperti. Dalam Swift 4 dan Swift 3.2, ini telah sangat disederhanakan, seperti diuraikan dalam Menggunakan Pengamatan Nilai-Kunci di 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)")
        }
    }
}

Catatan, dalam Swift 4, kita sekarang memiliki pengetikan keypath yang kuat menggunakan karakter backslash ( \.baradalah keypath untuk barproperti objek yang diamati). Juga, karena menggunakan pola penutupan penyelesaian, kita tidak harus menghapus pengamat secara manual (ketika tokenjatuh di luar ruang lingkup, pengamat dihapus untuk kita) juga tidak perlu khawatir tentang memanggil superimplementasi jika kuncinya tidak pertandingan. Penutupan ini dipanggil hanya ketika pengamat khusus ini dipanggil. Untuk informasi lebih lanjut, lihat video WWDC 2017, What's New in Foundation .

Dalam Swift 3, untuk mengamati ini, ini sedikit lebih rumit, tetapi sangat mirip dengan apa yang dilakukan di Objective-C. Yaitu, Anda akan mengimplementasikan observeValue(forKeyPath keyPath:, of object:, change:, context:)yang mana (a) memastikan kita berurusan dengan konteks kita (dan bukan sesuatu yang superterdaftar untuk diamati oleh contoh kita ); dan kemudian (b) menanganinya atau meneruskannya ke superimplementasi, sebagaimana diperlukan. Dan pastikan untuk melepaskan diri Anda sebagai pengamat saat yang tepat. Misalnya, Anda dapat menghapus pengamat saat itu tidak dialokasikan:

Dalam 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])")
    }

}

Catatan, Anda hanya bisa mengamati properti yang bisa direpresentasikan dalam Objective-C. Dengan demikian, Anda tidak dapat mengamati obat generik, structtipe Swift enum, tipe Swift , dll.

Untuk diskusi tentang implementasi Swift 2, lihat jawaban asli saya, di bawah ini.


Menggunakan dynamickata kunci untuk mencapai KVO dengan NSObjectsubkelas dijelaskan di bagian Pengamatan Nilai-Kunci pada bab Mengadopsi Konvensi Desain Kakao dari Menggunakan Swift dengan Kakao dan panduan Objective-C :

Pengamatan nilai-kunci adalah mekanisme yang memungkinkan objek untuk diberitahu tentang perubahan pada properti tertentu dari objek lain. Anda bisa menggunakan mengamati nilai kunci dengan kelas Swift, selama kelas tersebut mewarisi dari NSObjectkelas. Anda dapat menggunakan tiga langkah ini untuk menerapkan mengamati kunci-nilai di Swift.

  1. Tambahkan dynamicpengubah ke properti apa pun yang ingin Anda amati. Untuk informasi lebih lanjut tentang dynamic, lihat Membutuhkan Pengiriman Dinamis .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Buat variabel konteks global.

    private var myContext = 0
  3. Tambahkan pengamat untuk jalur-kunci, dan timpa observeValueForKeyPath:ofObject:change:context:metode, dan hapus pengamat di 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)
        }
    }

[Catatan, diskusi KVO ini kemudian telah dihapus dari panduan Menggunakan Swift dengan Kakao dan Objective-C , yang telah diadaptasi untuk Swift 3, tetapi masih berfungsi seperti yang dijabarkan di bagian atas jawaban ini.]


Perlu dicatat bahwa Swift memiliki sistem pengamat properti asli sendiri , tetapi itu untuk kelas yang menetapkan kode sendiri yang akan dilakukan setelah mengamati propertinya sendiri. KVO, di sisi lain, dirancang untuk mendaftar untuk mengamati perubahan pada beberapa properti dinamis dari beberapa kelas lain.

rampok
sumber
Apa tujuan myContextdan bagaimana Anda mengamati beberapa properti?
devth
1
Menurut Panduan Pemrograman KVO : "Ketika Anda mendaftarkan suatu objek sebagai pengamat, Anda juga dapat memberikan contextpointer. contextPointer diberikan kepada pengamat ketika observeValueForKeyPath:ofObject:change:context:dipanggil. contextPointer dapat berupa pointer C atau referensi objek. contextPointer dapat berupa digunakan sebagai pengidentifikasi unik untuk menentukan perubahan yang sedang diamati, atau untuk memberikan beberapa data lain kepada pengamat. "
Rob
Anda harus menghapus pengamat di deinit
Jacky
3
@devth, seperti yang saya mengerti, jika subclass atau superclass juga mendaftarkan pengamat KVO untuk variabel yang sama, observValueForKeyPath akan dipanggil beberapa kali. Konteks dapat digunakan untuk membedakan notifikasi sendiri dalam situasi ini. Lebih lanjut tentang ini: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey
1
Jika Anda membiarkannya optionskosong, itu hanya berarti bahwa changetidak akan menyertakan nilai lama atau baru (mis. Anda mungkin mendapatkan sendiri nilai baru dengan mereferensikan objek itu sendiri). Jika Anda hanya menentukan .newdan tidak .old, itu berarti bahwa changehanya akan memasukkan nilai baru, tetapi bukan nilai lama (misalnya Anda sering tidak peduli tentang apa nilai lama itu, tetapi hanya peduli tentang nilai baru). Jika Anda harus observeValueForKeyPathmemberi Anda nilai lama dan baru, maka tentukan [.new, .old]. Intinya, optionscukup tentukan apa yang termasuk dalam changekamus.
Rob
92

Baik ya dan tidak:

  • Ya , Anda bisa menggunakan KVO API lama yang sama di Swift untuk mengamati objek Objective-C.
    Anda juga dapat mengamati dynamicproperti objek Swift yang diwarisi darinya NSObject.
    Tapi ... Tidak, itu tidak diketik seperti yang Anda harapkan dari sistem observasi asli Swift.
    Menggunakan Swift dengan Kakao dan Objective-C | Mengamati Nilai Kunci

  • Tidak , saat ini tidak ada sistem observasi nilai bawaan untuk objek Swift yang arbitrer.

  • Ya , ada Pengamat Properti bawaan , yang sangat diketik.
    Tapi ... Tidak, mereka bukan KVO, karena hanya memungkinkan untuk mengamati properti milik objek, tidak mendukung pengamatan bersarang ("jalur utama"), dan Anda harus mengimplementasikannya secara eksplisit.
    Bahasa Pemrograman Swift | Pengamat Properti

  • Ya , Anda dapat menerapkan pengamatan nilai eksplisit, yang akan diketik dengan kuat, dan memungkinkan untuk menambahkan beberapa penangan dari objek lain, dan bahkan mendukung nesting / "jalur kunci".
    Tapi ... Tidak itu tidak akan KVO karena hanya akan bekerja untuk properti yang Anda terapkan sebagai diamati.
    Anda dapat menemukan perpustakaan untuk menerapkan mengamati nilai tersebut di sini:
    Observable-Swift - KVO for Swift - Value Observing and Events

slazyk
sumber
10

Contoh mungkin membantu sedikit di sini. Jika saya memiliki instance modelkelas Modeldengan atribut namedan statesaya dapat mengamati atribut tersebut dengan:

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)

Perubahan pada properti ini akan memicu panggilan ke:

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

        println("CHANGE OBSERVED: \(change)")
}
Paul Robinson
sumber
2
Jika saya tidak salah, pendekatan panggilan observValueForKeyPath adalah untuk Swift2.
Fattie
9

Iya.

KVO membutuhkan pengiriman dinamis, jadi Anda hanya perlu menambahkan dynamicpengubah ke metode, properti, subskrip, atau inisialisasi:

dynamic var foo = 0

The dynamicMemastikan pengubah bahwa referensi untuk deklarasi akan secara dinamis dikirim dan diakses melalui objc_msgSend.

Bryan Luby
sumber
7

Selain jawaban Rob. Kelas itu harus diwarisi dari NSObject, dan kami memiliki 3 cara untuk memicu perubahan properti

Gunakan setValue(value: AnyObject?, forKey key: String)dariNSKeyValueCoding

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

Gunakan willChangeValueForKeydan didChangeValueForKeydariNSKeyValueObserving

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

Gunakan dynamic. Lihat Kompatibilitas Jenis Swift

Anda juga dapat menggunakan pengubah dinamis untuk meminta akses kepada anggota dikirimkan secara dinamis melalui runtime Objective-C jika Anda menggunakan API seperti mengamati nilai kunci yang secara dinamis menggantikan implementasi metode.

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

Dan pengambil properti dan penyetel dipanggil saat digunakan. Anda dapat memverifikasi ketika bekerja dengan KVO. Ini adalah contoh dari properti yang dihitung

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
        }
    }
}
onmyway133
sumber
5

Gambaran

Dimungkinkan untuk menggunakan Combinetanpa menggunakan NSObjectatauObjective-C

Ketersediaan: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Catatan: Perlu digunakan hanya dengan kelas bukan dengan tipe nilai.

Kode:

Versi 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

Keluaran:

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

Lihat:

pengguna1046037
sumber
4

Saat ini Swift tidak mendukung mekanisme bawaan untuk mengamati perubahan properti objek selain 'diri', jadi tidak, itu tidak mendukung KVO.

Namun, KVO adalah bagian mendasar dari Objective-C dan Cocoa sehingga sangat mungkin untuk ditambahkan di masa depan. Dokumentasi saat ini tampaknya menyiratkan ini:

Mengamati Nilai-Kunci

Informasi yang akan datang.

Menggunakan Swift dengan Kakao dan Objective-C

ColinE
sumber
2
Jelas, panduan yang Anda referensi sekarang menjelaskan cara melakukan KVO di Swift.
Rob
4
Yap, sekarang diimplementasikan pada September 2014
Max MacLeod
4

Satu hal penting untuk disebutkan adalah bahwa setelah memperbarui Xcode Anda ke 7 beta Anda mungkin mendapatkan pesan berikut: "Metode tidak menimpa metode apa pun dari superclass-nya" . Itu karena opsionalitas argumen. Pastikan pawang pengamatan Anda terlihat persis seperti berikut:

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>)
Arthur Gevorkyan
sumber
2
Dalam Xcode beta 6 diperlukan: override func observValueForKeyPath (keyPath: String ?, ofObject objek: AnyObject ?, ubah: [String: AnyObject] ?, konteks: UnsafeMutablePointer <Void>)
hcanfly
4

Ini mungkin bermanfaat bagi beberapa orang -

// 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
        }
    }
}

Saya telah menggunakan KVO dengan cara ini di Swift 3. Anda dapat menggunakan kode ini dengan beberapa perubahan.

Nikhil Manapure
sumber
1

Contoh lain bagi siapa saja yang mengalami masalah dengan jenis seperti Int? dan CGFloat ?. Anda cukup mengatur kelas Anda sebagai subkelas NSObject dan mendeklarasikan variabel Anda sebagai berikut, misalnya:

class Theme : NSObject{

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

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

}
DrPatience
sumber