การตรวจสอบค่าคีย์ (KVO) มีให้ใน Swift หรือไม่


174

ถ้าเป็นเช่นนั้นมีความแตกต่างที่สำคัญใด ๆ ที่ไม่ได้ปรากฏเมื่อใช้การสังเกตค่าคีย์ใน Objective-C


2
ตัวอย่างโครงการที่แสดงให้เห็นถึงการใช้งาน KVO ในส่วนต่อประสาน UIKit ผ่าน Swift: github.com/jameswomack/kvo-in-swift
james_womack

@JanDvorak ดูคู่มือการเขียนโปรแกรม KVOซึ่งเป็นการแนะนำที่ดีสำหรับหัวข้อ
Rob

1
แม้ว่าจะไม่ใช่คำตอบสำหรับคำถามของคุณ แต่คุณสามารถเริ่มดำเนินการได้โดยใช้ฟังก์ชัน didset ()
Vincent

หมายเหตุมี Swift4 ข้อผิดพลาด.initialเมื่อคุณใช้ สำหรับการแก้ปัญหาดูที่นี่ ผมขอแนะนำให้ไปดูเอกสารแอปเปิ้ล เพิ่งได้รับการอัปเดตเมื่อเร็ว ๆ นี้และครอบคลุมบันทึกย่อที่สำคัญมากมาย ดูคำตอบอื่น ๆ
Honey

คำตอบ:


107

(แก้ไขเพื่อเพิ่มข้อมูลใหม่): พิจารณาว่าการใช้กรอบการทำงานร่วมกันสามารถช่วยให้คุณบรรลุสิ่งที่คุณต้องการแทนที่จะใช้ KVO หรือไม่

ใช่และไม่. KVO ทำงานกับคลาสย่อย NSObject ได้มากที่สุด มันไม่ทำงานสำหรับคลาสที่ไม่มีคลาสย่อย NSObject สวิฟท์ไม่มี (อย่างน้อยในปัจจุบัน) มีระบบการสังเกตการณ์ของตนเอง

(ดูความคิดเห็นสำหรับวิธีเปิดเผยคุณสมบัติอื่น ๆ เช่น ObjC เพื่อให้ KVO ทำงานกับมันได้)

ดูเอกสารประกอบของ Appleสำหรับตัวอย่างเต็มรูปแบบ


74
ตั้งแต่ Xcode 6 beta 5 คุณสามารถใช้dynamicคำหลักในคลาส Swift เพื่อเปิดใช้งานการสนับสนุน KVO
fabb

7
ไชโยสำหรับ @fabb! เพื่อความชัดเจนdynamicคำหลักจะไปที่คุณสมบัติที่คุณต้องการทำให้คีย์ - ค่า - สามารถสังเกตได้
Jerry

5
คำอธิบายสำหรับdynamicคำหลักที่สามารถพบได้ในแอปเปิ้ลผู้พัฒนาห้องสมุดของการใช้สวิฟท์กับโกโก้และวัตถุประสงค์ส่วน C
Imanou Petit

6
เนื่องจากสิ่งนี้ไม่ชัดเจนสำหรับฉันจากความคิดเห็นของ @ fabb: ใช้dynamicคำหลักสำหรับคุณสมบัติใด ๆภายในคลาสที่คุณต้องการให้เป็นไปตามมาตรฐาน KVO (ไม่ใช่dynamicคำหลักในคลาสเอง) สิ่งนี้ได้ผลสำหรับฉัน!
Tim Camber

1
ไม่ได้จริงๆ คุณไม่สามารถลงทะเบียน didSet ใหม่จาก "นอก" มันจะต้องเป็นส่วนหนึ่งของประเภทนั้นในเวลารวบรวม
Catfish_Man

155

คุณสามารถใช้ KVO ใน Swift แต่สำหรับdynamicคุณสมบัติของNSObjectคลาสย่อยเท่านั้น พิจารณาว่าคุณต้องการสังเกตbarคุณสมบัติของFooคลาส ใน Swift 4 ให้ระบุว่าbarเป็นdynamicคุณสมบัติในNSObjectคลาสย่อยของคุณ:

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 ตอนนี้เรามีการพิมพ์ที่สำคัญของ keypaths โดยใช้อักขระเครื่องหมายทับขวา ( \.barคือ keypath สำหรับbarคุณสมบัติของวัตถุที่กำลังสังเกตเห็น) นอกจากนี้เนื่องจากใช้รูปแบบการปิดเสร็จสิ้นเราไม่จำเป็นต้องลบผู้สังเกตการณ์ด้วยตนเอง (เมื่อtokenอยู่นอกขอบเขตผู้สังเกตการณ์จะถูกลบสำหรับเรา) และเราไม่ต้องกังวลเกี่ยวกับการเรียกsuperใช้งานหากไม่มี การจับคู่. การปิดถูกเรียกก็ต่อเมื่อผู้สังเกตการณ์คนนี้ถูกเรียกใช้ สำหรับข้อมูลเพิ่มเติมโปรดดูที่ WWDC 2017 วิดีโอมีอะไรใหม่ในมูลนิธิ

ใน Swift 3 เพื่อสังเกตสิ่งนี้มันซับซ้อนกว่าเล็กน้อย แต่คล้ายกับสิ่งที่ทำใน Objective-C กล่าวคือคุณจะใช้observeValue(forKeyPath keyPath:, of object:, change:, context:)ซึ่ง (a) ทำให้แน่ใจว่าเรากำลังเผชิญกับบริบทของเรา (และไม่ใช่สิ่งที่superอินสแตนซ์ของเราได้ลงทะเบียนเพื่อสังเกต); จากนั้น (b) จัดการหรือส่งต่อไปยังsuperการนำไปปฏิบัติตามความจำเป็น และให้แน่ใจว่าจะลบตัวคุณเองเป็นผู้สังเกตการณ์เมื่อเหมาะสม ตัวอย่างเช่นคุณอาจลบผู้สังเกตการณ์เมื่อถูกยกเลิกการจัดสรร:

ในสวิฟต์ 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 เท่านั้น ดังนั้นคุณไม่สามารถสังเกตข้อมูลทั่วไป, structประเภทสวิฟต์enum, ประเภทสวิฟต์เป็นต้น

สำหรับการอภิปรายเกี่ยวกับการใช้งาน Swift 2 ดูคำตอบต้นฉบับของฉันด้านล่าง


การใช้dynamicคำหลักเพื่อให้บรรลุ KVO ด้วยNSObjectคลาสย่อยนั้นได้อธิบายไว้ในส่วนการสังเกตคีย์ - ค่าของการยอมรับแนวคิดการออกแบบโคโค่ในบทของการใช้ Swift กับโกโก้และวัตถุประสงค์ -C :

การสังเกตค่าคีย์เป็นกลไกที่อนุญาตให้วัตถุได้รับแจ้งถึงการเปลี่ยนแปลงคุณสมบัติที่ระบุของวัตถุอื่น คุณสามารถใช้คีย์ - ค่าการสังเกตด้วยคลาส Swift ตราบใดที่คลาสนั้นสืบทอดมาจากNSObjectคลาส คุณสามารถใช้สามขั้นตอนเหล่านี้เพื่อนำการสังเกตค่าคีย์ไปใช้ใน 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 กับ Cocoa และ Objective-Cซึ่งได้รับการดัดแปลงสำหรับ Swift 3 แต่ก็ยังใช้งานได้ตามที่ระบุไว้ด้านบนของคำตอบนี้]


เป็นที่น่าสังเกตว่า Swift มีระบบสังเกตการณ์คุณสมบัติของตนเองแต่สำหรับคลาสที่ระบุรหัสของตัวเองที่จะดำเนินการเมื่อสังเกตคุณสมบัติของตัวเอง ในทางกลับกัน KVO นั้นถูกออกแบบมาเพื่อลงทะเบียนเพื่อสังเกตการเปลี่ยนแปลงคุณสมบัติบางอย่างของคลาสอื่น ๆ


มีจุดประสงค์อะไรmyContextและคุณสังเกตคุณสมบัติหลายอย่างได้อย่างไร
devth

1
ตามคู่มือการเขียนโปรแกรมของ KVO : "เมื่อคุณลงทะเบียนวัตถุเป็นผู้สังเกตการณ์คุณสามารถระบุcontextตัวชี้ได้contextตัวชี้ถูกจัดเตรียมให้กับผู้สังเกตการณ์เมื่อobserveValueForKeyPath:ofObject:change:context:มีการเรียกใช้contextตัวชี้อาจเป็นตัวชี้ C หรือการอ้างอิงวัตถุcontextตัวชี้สามารถ ใช้เป็นตัวระบุที่ไม่ซ้ำกันเพื่อพิจารณาการเปลี่ยนแปลงที่กำลังถูกสังเกตหรือเพื่อให้ข้อมูลอื่น ๆ แก่ผู้สังเกตการณ์ "
Rob

คุณต้องลบผู้สังเกตการณ์ใน deinit
Jacky

3
@devth ตามที่ฉันเข้าใจถ้า subclass หรือ superclass ยังทำการลงทะเบียนผู้สังเกตการณ์ KVO สำหรับตัวแปรเดียวกันนี้ด้วยสังเกตการณ์ ValueForKeyPath จะถูกเรียกหลายครั้ง บริบทสามารถใช้แยกแยะการแจ้งเตือนของตัวเองในสถานการณ์นี้ เพิ่มเติมเกี่ยวกับเรื่องนี้: 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

ทั้งใช่และไม่ใช่:

  • ได้คุณสามารถใช้ KVO APIs เดิมใน Swift เพื่อสังเกตการณ์ Objective-C
    คุณสามารถสังเกตdynamicคุณสมบัติของวัตถุ Swift ที่สืบทอดมาNSObjectได้
    แต่ ... ไม่มันไม่ได้ถูกพิมพ์ออกมาอย่างแรงเท่าที่คุณคาดหวังจากระบบการสังเกตการณ์ของ Swift
    ใช้ Swift กับ Cocoa และ Objective-C | ค่าคีย์การสังเกต

  • ไม่ปัจจุบันไม่มีระบบการสังเกตค่าบิวอินสำหรับวัตถุ Swift ตามอำเภอใจ

  • ใช่มีบิวด์อินObservers คุณสมบัติซึ่งพิมพ์อย่างมาก
    แต่ ... ไม่ใช่พวกเขาไม่ใช่ KVO เนื่องจากอนุญาตให้ทำการตรวจสอบคุณสมบัติของวัตถุเท่านั้นไม่สนับสนุนการตรวจสอบที่ซ้อนกัน ("เส้นทางที่สำคัญ") และคุณต้องนำไปใช้อย่างชัดเจน
    ภาษาโปรแกรม Swift ผู้สังเกตการณ์อสังหาริมทรัพย์

  • ใช่คุณสามารถใช้การสังเกตค่าอย่างชัดเจนซึ่งจะถูกพิมพ์อย่างมากและอนุญาตให้เพิ่มตัวจัดการหลายตัวจากวัตถุอื่น ๆ และแม้กระทั่งรองรับการซ้อน / "เส้นทางที่สำคัญ"
    แต่ ... ไม่มันจะไม่ใช่ KVO เพราะมันจะใช้ได้กับคุณสมบัติที่คุณใช้งานได้เท่านั้น
    คุณสามารถค้นหาไลบรารีสำหรับการนำค่าดังกล่าวไปใช้ที่นี่:
    Observable-Swift - KVO for Swift - การสังเกตค่าและเหตุการณ์


10

ตัวอย่างอาจช่วยเล็กน้อยที่นี่ หากฉันมีอินสแตนซ์modelของคลาสที่Modelมีคุณลักษณะnameและstateฉันสามารถสังเกตคุณสมบัติเหล่านั้นด้วย:

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
ถ้าฉันไม่เข้าใจผิดวิธีการสังเกตการณ์ค่าโทรแบบคีย์คี ธ สำหรับ Swift2
Fattie

9

ใช่.

KVO ต้องการการแจกจ่ายแบบไดนามิกดังนั้นคุณเพียงแค่ต้องเพิ่มdynamicตัวปรับเปลี่ยนไปยังวิธีการคุณสมบัติตัวห้อยหรือตัวเริ่มต้น:

dynamic var foo = 0

เพื่อให้แน่ใจว่าการปรับปรุงการอ้างอิงถึงการประกาศจะถูกส่งแบบไดนามิกและการเข้าถึงผ่านdynamicobjc_msgSend


7

นอกจากคำตอบของ Rob แล้ว คลาสนั้นต้องสืบทอดมา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 Type

คุณสามารถใช้โมดิฟายเออร์แบบไดนามิกเพื่อกำหนดให้การเข้าถึงสมาชิกนั้นส่งผ่านไดนามิกรันไทม์ของ Objective-C หากคุณใช้ API เช่นคีย์ - ค่าการสังเกตว่าแทนที่การใช้เมธอดแบบไดนามิก

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

และคุณสมบัติทะเยอทะยานและ Setter เรียกว่าเมื่อใช้ คุณสามารถตรวจสอบได้เมื่อทำงานกับ 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

ปัจจุบันสวิฟท์ไม่รองรับกลไกใด ๆ ในตัวสำหรับการสังเกตการเปลี่ยนแปลงคุณสมบัติของวัตถุอื่นนอกเหนือจาก 'ตัวตน' ดังนั้นไม่มันไม่รองรับ KVO

อย่างไรก็ตาม KVO เป็นส่วนพื้นฐานของ Objective-C และ Cocoa ที่ดูเหมือนว่าค่อนข้างน่าจะเพิ่มเข้ามาในอนาคต เอกสารปัจจุบันดูเหมือนจะบอกเป็นนัยถึงสิ่งนี้:

การสังเกตมูลค่าคีย์

ข้อมูลเตรียมพร้อม

ใช้ Swift กับ Cocoa และ Objective-C


2
เห็นได้ชัดว่าคู่มือที่คุณอ้างอิงตอนนี้อธิบายถึงวิธีการทำ KVO ใน Swift
Rob

4
ใช่ตอนนี้ใช้งานตั้งแต่เดือนกันยายน 2014
Max MacLeod

4

สิ่งหนึ่งที่สำคัญที่จะกล่าวถึงคือว่าหลังจากที่การอัปเดตของคุณXcodeไป7 เบต้าคุณอาจจะได้รับข้อความต่อไปนี้: "วิธีการไม่ได้แทนที่วิธีใด ๆ จาก superclass ของมัน" นั่นเป็นเพราะตัวเลือกของข้อโต้แย้ง ตรวจสอบให้แน่ใจว่าตัวจัดการการสังเกตของคุณมีลักษณะดังนี้:

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

2
ใน Xcode เบต้า 6 ต้องใช้: แทนที่ func observValueForKeyPath (keyPath: String ?,Object วัตถุ: AnyObject ?, เปลี่ยนแปลง: [String: AnyObject]?, บริบท: 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
        }
    }
}

ฉันใช้ KVO ด้วยวิธีนี้ใน Swift 3 คุณสามารถใช้รหัสนี้โดยมีการเปลี่ยนแปลงเล็กน้อย


1

อีกตัวอย่างหนึ่งสำหรับทุกคนที่พบปัญหากับประเภทเช่น Int? และ 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.