Quan sát giá trị khóa (KVO) có khả dụng trong Swift không?


174

Nếu vậy, có sự khác biệt nào về khóa mà không có mặt khi sử dụng quan sát giá trị khóa trong Objective-C không?


2
Một dự án ví dụ chứng minh KVO đang được sử dụng trong giao diện UIKit qua Swift: github.com/jameswomack/kvo-in-swift
james_womack

@JanDvorak Xem Hướng dẫn lập trình KVO , đây là một giới thiệu hay về chủ đề này.
Rob

1
Mặc dù không phải là câu trả lời cho câu hỏi của bạn, bạn cũng có thể bắt đầu hành động bằng cách sử dụng hàm didset ().
Vincent

Lưu ý có lỗi Swift4 khi bạn sử dụng .initial. Đối với một giải pháp xem tại đây . Tôi rất khuyên bạn nên xem tài liệu của Apple . Nó đã được cập nhật gần đây và bao gồm rất nhiều ghi chú quan trọng. Cũng xem câu trả lời khác
Mật ong

Câu trả lời:


107

(Đã chỉnh sửa để thêm thông tin mới): xem xét liệu sử dụng khung Kết hợp có thể giúp bạn thực hiện những gì bạn muốn, thay vì sử dụng KVO

Có và không. KVO hoạt động trên các lớp con NSObject nhiều như nó luôn có. Nó không hoạt động đối với các lớp không phân lớp NSObject. Swift không (hiện tại ít nhất) có hệ thống quan sát riêng của mình.

(Xem bình luận về cách hiển thị các thuộc tính khác như ObjC để KVO hoạt động trên chúng)

Xem Tài liệu Apple để biết ví dụ đầy đủ.


74
Vì Xcode 6 beta 5, bạn có thể sử dụng dynamictừ khóa trên bất kỳ lớp Swift nào để bật hỗ trợ KVO.
fabb

7
Hoan hô cho @fabb! Để rõ ràng, dynamictừ khóa đi vào thuộc tính mà bạn muốn làm cho khóa-giá trị có thể quan sát được.
Jerry

5
dynamicthể tìm thấy lời giải thích cho từ khóa trong phần Sử dụng Swift với Ca cao và Mục tiêu-C của Thư viện Nhà phát triển Apple .
Imanou Petit

6
Vì điều này không rõ ràng với tôi từ nhận xét của @ fabb: sử dụng dynamictừ khóa cho bất kỳ thuộc tính nào trong một lớp mà bạn muốn tuân thủ KVO (không phải dynamictừ khóa trên chính lớp đó). Điều này làm việc cho tôi!
Tim Camber

1
Không thực sự; bạn không thể đăng ký một didset mới từ "bên ngoài", nó phải là một phần của loại đó vào thời gian biên dịch.
Catfish_Man

155

Bạn có thể sử dụng KVO trong Swift, nhưng chỉ cho dynamiccác thuộc tính của NSObjectlớp con. Hãy xem xét rằng bạn muốn quan sát bartài sản của một Foolớp. Trong Swift 4, chỉ định bardynamicthuộc tính trong NSObjectlớp con của bạn :

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

Sau đó bạn có thể đăng ký để quan sát các thay đổi đối với bartài sản. Trong Swift 4 và Swift 3.2, điều này đã được đơn giản hóa rất nhiều, như được nêu trong Sử dụng Quan sát giá trị khóa trong 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)")
        }
    }
}

Lưu ý, trong Swift 4, giờ đây chúng ta đã gõ mạnh các phím bấm bằng cách sử dụng ký tự dấu gạch chéo ngược ( \.barlà đường dẫn phím cho thuộc bartính của đối tượng được quan sát). Ngoài ra, vì sử dụng mẫu đóng hoàn thành, chúng tôi không phải xóa thủ công (khi tokenrơi ra khỏi phạm vi, người quan sát sẽ bị xóa đối với chúng tôi) và chúng tôi không phải lo lắng về việc gọi supertriển khai nếu khóa không trận đấu. Việc đóng cửa chỉ được gọi khi người quan sát cụ thể này được gọi. Để biết thêm thông tin, hãy xem video WWDC 2017, Có gì mới trong Foundation .

Trong Swift 3, để quan sát điều này, nó phức tạp hơn một chút, nhưng rất giống với những gì người ta làm trong Objective-C. Cụ thể, bạn sẽ triển khai observeValue(forKeyPath keyPath:, of object:, change:, context:)(a) đảm bảo rằng chúng tôi xử lý bối cảnh của chúng tôi (và không phải là điều mà supercá thể của chúng tôi đã đăng ký để quan sát); và sau đó (b) hoặc xử lý nó hoặc chuyển nó cho việc superthực hiện, khi cần thiết. Và hãy chắc chắn để loại bỏ bản thân như một người quan sát khi thích hợp. Ví dụ: bạn có thể xóa trình quan sát khi nó bị hủy:

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

}

Lưu ý, bạn chỉ có thể quan sát các thuộc tính có thể được biểu diễn trong Objective-C. Do đó, bạn không thể quan sát thuốc generic, structloại Swift enum, loại Swift , v.v.

Để thảo luận về việc triển khai Swift 2, hãy xem câu trả lời ban đầu của tôi, bên dưới.


Sử dụng dynamictừ khóa để đạt được KVO với các NSObjectlớp con được mô tả trong phần Quan sát giá trị khóa của chương Các quy ước thiết kế ca cao thông qua của Hướng dẫn sử dụng Swift với ca cao và mục tiêu-C :

Quan sát giá trị khóa là một cơ chế cho phép các đối tượng được thông báo về các thay đổi đối với các thuộc tính được chỉ định của các đối tượng khác. Bạn có thể sử dụng quan sát khóa-giá trị với một lớp Swift, miễn là lớp kế thừa từ NSObjectlớp đó. Bạn có thể sử dụng ba bước này để triển khai quan sát khóa-giá trị trong Swift.

  1. Thêm công cụ dynamicsửa đổi vào bất kỳ tài sản nào bạn muốn quan sát. Để biết thêm thông tin về dynamic, xem Yêu cầu công văn động .

    class MyObjectToObserve: NSObject {
        dynamic var myDate = NSDate()
        func updateDate() {
            myDate = NSDate()
        }
    }
  2. Tạo một biến bối cảnh toàn cầu.

    private var myContext = 0
  3. Thêm một trình quan sát cho đường dẫn khóa và ghi đè observeValueForKeyPath:ofObject:change:context:phương thức và xóa trình quan sát trong 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)
        }
    }

[Lưu ý, cuộc thảo luận KVO này sau đó đã bị xóa khỏi hướng dẫn Sử dụng Swift với Ca cao và Mục tiêu-C , đã được điều chỉnh cho Swift 3, nhưng nó vẫn hoạt động như được nêu ở đầu câu trả lời này.]


Điều đáng chú ý là Swift có hệ thống quan sát thuộc tính riêng của mình , nhưng đó là cho một lớp chỉ định mã riêng của nó sẽ được thực hiện khi quan sát các thuộc tính của chính nó. Mặt khác, KVO được thiết kế để đăng ký để quan sát các thay đổi đối với một số thuộc tính động của một số lớp khác.


Mục đích của myContextvà làm thế nào để bạn quan sát nhiều thuộc tính?
devth

1
Theo Hướng dẫn lập trình KVO : "Khi bạn đăng ký một đối tượng làm người quan sát, bạn cũng có thể cung cấp một contextcon trỏ. Con contexttrỏ được cung cấp cho người quan sát khi observeValueForKeyPath:ofObject:change:context:được gọi. Con contexttrỏ có thể là con trỏ C hoặc tham chiếu đối tượng. Con contexttrỏ có thể là con trỏ được sử dụng như một định danh duy nhất để xác định thay đổi đang được quan sát hoặc để cung cấp một số dữ liệu khác cho người quan sát. "
Rob

bạn cần xóa người quan sát trong deinit
Jacky

3
@devth, theo tôi hiểu, nếu lớp con hoặc siêu lớp cũng đăng ký người quan sát KVO cho cùng một biến, obsValueForKeyPath sẽ được gọi nhiều lần. Bối cảnh có thể được sử dụng để phân biệt các thông báo riêng trong tình huống này. Thông tin thêm về điều này: dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage
Zmey

1
Nếu bạn để optionstrống, điều đó chỉ có nghĩa là changesẽ không bao gồm giá trị cũ hoặc mới (ví dụ: bạn có thể tự nhận giá trị mới bằng cách tham chiếu chính đối tượng). Nếu bạn chỉ xác định .newvà không .old, điều đó có nghĩa là changesẽ chỉ bao gồm giá trị mới, nhưng không bao gồm giá trị cũ (ví dụ: bạn thường không quan tâm đến giá trị cũ là gì, mà chỉ quan tâm đến giá trị mới). Nếu bạn cần observeValueForKeyPathchuyển cho bạn cả giá trị cũ và mới, sau đó chỉ định [.new, .old]. Dòng dưới cùng, optionschỉ định những gì được bao gồm trong changetừ điển.
Cướp

92

Cả có và không:

  • , bạn có thể sử dụng các API KVO cũ tương tự trong Swift để quan sát các đối tượng Objective-C.
    Bạn cũng có thể quan sát dynamiccác thuộc tính của các đối tượng Swift kế thừa từ NSObject.
    Nhưng ... Không, nó không được gõ mạnh như bạn có thể mong đợi hệ thống quan sát bản địa của Swift.
    Sử dụng Swift với Ca cao và Mục tiêu-C | Quan sát giá trị chính

  • Không , hiện tại không có hệ thống quan sát giá trị dựng sẵn cho các đối tượng Swift tùy ý.

  • , có Trình quan sát tài sản dựng sẵn , được gõ mạnh.
    Nhưng ... Không, chúng không phải là KVO, vì chúng chỉ cho phép quan sát các thuộc tính của đối tượng, không hỗ trợ các quan sát lồng nhau ("đường dẫn chính") và bạn phải thực hiện chúng một cách rõ ràng.
    Ngôn ngữ lập trình Swift | Quan sát tài sản

  • , bạn có thể thực hiện quan sát giá trị rõ ràng, sẽ được gõ mạnh và cho phép thêm nhiều trình xử lý từ các đối tượng khác và thậm chí hỗ trợ lồng / "đường dẫn chính".
    Nhưng ... Không, nó sẽ không phải là KVO vì nó sẽ chỉ hoạt động đối với các thuộc tính mà bạn triển khai là có thể quan sát được.
    Bạn có thể tìm thấy một thư viện để triển khai giá trị quan sát như vậy tại đây:
    Observable-Swift - KVO for Swift - Quan sát giá trị và sự kiện


10

Một ví dụ có thể giúp một chút ở đây. Nếu tôi có một thể hiện modelcủa lớp Modelvới các thuộc tính namestatetôi có thể quan sát các thuộc tính đó bằng:

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)

Thay đổi đối với các thuộc tính này sẽ kích hoạt lệnh gọi tới:

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

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

2
Nếu tôi không nhầm, cách tiếp cận cuộc gọi obsValueForKeyPath là dành cho Swift2.
Fattie

9

Đúng.

KVO yêu cầu công văn động, vì vậy bạn chỉ cần thêm công cụ dynamicsửa đổi vào phương thức, thuộc tính, chỉ mục hoặc trình khởi tạo:

dynamic var foo = 0

Công cụ dynamicsửa đổi đảm bảo rằng các tham chiếu đến khai báo sẽ được gửi động và truy cập thông qua objc_msgSend.


7

Ngoài câu trả lời của Rob. Lớp đó phải kế thừa từ NSObjectvà chúng tôi có 3 cách để kích hoạt thay đổi thuộc tính

Sử dụng setValue(value: AnyObject?, forKey key: String)từNSKeyValueCoding

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

Sử dụng willChangeValueForKeydidChangeValueForKeytừNSKeyValueObserving

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

Sử dụng dynamic. Xem Tương thích loại Swift

Bạn cũng có thể sử dụng công cụ sửa đổi động để yêu cầu quyền truy cập vào các thành viên được gửi động qua thời gian chạy Objective-C nếu bạn đang sử dụng các API như giá trị key key quan sát thay thế động việc thực hiện phương thức.

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

Và thuộc tính getter và setter được gọi khi sử dụng. Bạn có thể xác minh khi làm việc với KVO. Đây là một ví dụ về tài sản tính toán

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

Tổng quat

Có thể sử dụng Combinemà không cần sử dụng NSObjecthoặcObjective-C

Tình trạng sẵn có: iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

Lưu ý: Chỉ cần sử dụng với các lớp không có loại giá trị.

Mã số:

Phiên bản 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

Đầu ra:

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

Tham khảo:


4

Hiện tại Swift không hỗ trợ bất kỳ cơ chế tích hợp nào để quan sát các thay đổi thuộc tính của các đối tượng khác ngoài 'bản thân', vì vậy không, nó không hỗ trợ KVO.

Tuy nhiên, KVO là một phần cơ bản của Objective-C và Ca cao đến mức có vẻ như nó sẽ được thêm vào trong tương lai. Các tài liệu hiện tại dường như ngụ ý điều này:

Quan sát giá trị khóa

Thông tin sắp tới.

Sử dụng Swift với Ca cao và Mục tiêu-C


2
Rõ ràng, hướng dẫn mà bạn tham khảo bây giờ mô tả cách thực hiện KVO trong Swift.
Rob

4
Đúng, hiện được triển khai kể từ tháng 9 năm 2014
Max MacLeod

4

Một điều quan trọng cần đề cập là sau khi cập nhật Xcode của bạn lên 7 beta, bạn có thể nhận được thông báo sau: "Phương thức không ghi đè bất kỳ phương thức nào từ siêu lớp của nó" . Đó là vì tính tùy chọn của các đối số. Hãy chắc chắn rằng trình xử lý quan sát của bạn trông chính xác như sau:

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

2
Trong Xcode beta 6, nó yêu cầu: ghi đè func obsValueForKeyPath (keyPath: String?, OfObject object: AnyObject?, Change: [String: AnyObject]?, Bối cảnh: UnsafeMutablePulum <Void>)
hcanfly

4

Điều này có thể được chứng minh là hữu ích với ít người -

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

Tôi đã sử dụng KVO theo cách này trong Swift 3. Bạn có thể sử dụng mã này với một vài thay đổi.


1

Một ví dụ khác cho bất cứ ai gặp vấn đề với các loại như Int? và CGFloat?. Bạn chỉ cần đặt lớp của mình làm lớp con của NSObject và khai báo các biến của mình như sau: vd:

class Theme : NSObject{

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

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

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.