Swift에서 키-값 관찰 (KVO)이 가능합니까?


174

그렇다면 Objective-C에서 키-값 관찰을 사용할 때 달리 존재하지 않는 주요 차이점이 있습니까?


2
Swift를 통해 UIKit 인터페이스에서 KVO를 사용하는 방법을 보여주는 예제 프로젝트 : github.com/jameswomack/kvo-in-swift
james_womack 1

@JanDvorak 주제에 대한 좋은 소개 인 KVO 프로그래밍 안내서를 참조하십시오 .
Rob

1
질문에 대한 답변은 아니지만 didset () 함수를 사용하여 작업을 시작할 수도 있습니다.
Vincent

를 사용할 때 Swift4 버그 가 있습니다 .initial. 해결책은 여기를 참조 하십시오 . 애플 문서 를 볼 것을 적극 권장합니다 . 최근에 업데이트되었으며 많은 중요한 메모를 다룹니다. Rob의 다른 답변
Honey

답변:


107

(새로운 정보를 추가하기 위해 편집 됨) : KVO를 사용하지 않고 Combine 프레임 워크를 사용하여 원하는 것을 달성 할 수 있는지 고려하십시오.

예, 아니오 KVO는 NSObject 서브 클래스에서 항상 그렇듯이 작동합니다. NSObject를 서브 클래스하지 않는 클래스에는 작동하지 않습니다. 스위프트는 (현재는 최소한) 고유의 관측 시스템을 가지고 있지 않습니다.

(KVO가 작동하도록 ObjC로 다른 속성을 노출하는 방법에 대한 의견 참조)

전체 예 는 Apple 설명서 를 참조하십시오 .


74
Xcode 6 베타 5부터 dynamic모든 Swift 클래스 에서 키워드를 사용하여 KVO 지원을 활성화 할 수 있습니다 .
fabb

7
@fabb에 대한 만세! 명확성을 위해 dynamic키워드는 키-값을 관찰 가능하게하려는 속성으로 이동합니다.
Jerry

5
dynamic키워드에 대한 설명 은 Apple Developer Library의 Cocoa 및 Objective-C와 함께 Swift 사용 섹션 에서 찾을 수 있습니다 .
Imanou Petit

6
이것은 @fabb의 의견에서 명확하지 않았으므로 KVO를 준수하려는 클래스 내부의 속성에 대해 dynamic키워드를 사용하십시오 ( 클래스 자체 의 키워드가 아님). 이것은 나를 위해 일했다! dynamic
Tim Camber

1
실제로는 아닙니다. "outside"에서 새 didSet을 등록 할 수 없으며 컴파일시 해당 유형의 일부 여야합니다.
Catfish_Man 2016 년

155

Swift에서 KVO를 사용할 수 있지만 하위 클래스의 dynamic속성 에만 사용할 수 있습니다 NSObject. 클래스 의 bar속성 을 관찰하고 싶다고 생각하십시오 Foo. 스위프트 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에서는 백 슬래시 문자를 사용 \.bar하여 키 경로를 강력하게 입력했습니다 (이는 bar관찰되는 객체 의 속성에 대한 키 경로입니다 ). 또한 완료 클로저 패턴을 사용하기 때문에 관찰자를 수동으로 제거 할 필요가 없으며 ( token범위를 벗어나면 관찰자가 제거됩니다) super키가 아닌 경우 구현 호출에 대해 걱정할 필요 가 없습니다 시합. 클로저는이 특정 옵저버가 호출 될 때만 호출됩니다. 자세한 내용은 WWDC 2017 비디오, Foundation의 새로운 기능을 참조하십시오 .

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로 표현할 수있는 특성 만 관찰 할 수 있습니다. 따라서 제네릭, Swift struct유형, Swift enum유형 등을 관찰 할 수 없습니다 .

Swift 2 구현에 대한 논의는 아래의 원래 답변을 참조하십시오.


서브 클래스로 dynamicKVO를 달성하기 위해 키워드를 사용하는 방법 NSObjectCocoa 및 Objective-C와 함께 Swift 사용 안내서코코아 설계 규칙 채택 장의 키-값 관찰 섹션에 설명되어 있습니다 .

키-값 관찰은 다른 객체의 지정된 속성에 대한 변경 사항을 객체에 통보 할 수있는 메커니즘입니다. 클래스가 클래스에서 상속되는 한 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 3에 맞게 조정 된 Cocoa 및 Objective-C와 함께 Swift 사용 가이드 에서 제거 되었지만 여전히이 답변의 맨 위에 설명 된대로 작동합니다.]


Swift는 자체 고유 속성 관찰 시스템을 가지고 있지만 자체 속성을 관찰 할 때 자체 코드를 지정하는 클래스입니다. 반면 KVO는 다른 클래스의 동적 속성에 대한 변경 사항을 관찰하도록 등록하도록 설계되었습니다.


myContext여러 속성 의 목적은 무엇 이며 어떻게 여러 속성을 관찰합니까?
devth

1
KVO 프로그래밍 가이드 에 따르면 : "객체를 옵저버로 등록 할 때 context포인터를 제공 할 수도 있습니다 . context포인터 observeValueForKeyPath:ofObject:change:context:는 호출 될 때 옵저버에게 제공 됩니다. context포인터는 C 포인터 또는 객체 참조 일 context수 있습니다 . 포인터는 관찰중인 변경 사항을 결정하거나 관찰자에게 다른 데이터를 제공하기 위해 고유 식별자로 사용됩니다. "
Rob

deinit에서 관찰자를 제거해야합니다
Jacky

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]. 결론 optionschange사전에 포함 된 내용 만 지정합니다 .
Rob

92

예와 아니오 :

  • , Swift에서 동일한 이전 KVO API를 사용하여 Objective-C 객체를 관찰 할 수 있습니다. 에서 상속되는 Swift 객체의 속성을
    관찰 할 수도 있습니다 . 그러나 ... 아니요 스위프트 기본 관측 시스템을 기대할 수있는 강력한 형식은 아닙니다. Cocoa 및 Objective-C와 함께 Swift 사용 | 주요 가치 관찰dynamicNSObject

  • 아니오 , 현재 임의의 Swift 객체에 대한 내장 값 관찰 시스템이 없습니다.

  • , 내장이있는 속성 관찰자 강력한 형식있다.
    그러나 ... 아니요. 객체 자체 속성 만 관찰 할 수 있고 중첩 된 관찰 ( "키 경로")을 지원하지 않으므로 명시 적으로 구현해야하므로 KVO가 아닙니다.
    스위프트 프로그래밍 언어 | 부동산 관찰자

  • , 명시 적 값 관찰을 구현할 수 있습니다. 강력한 관찰이 가능하며 다른 객체에서 여러 핸들러를 추가 할 수 있으며 중첩 / "키 경로"도 지원할 수 있습니다.
    그러나 ... 아니요 . 관찰 가능한 것으로 구현하는 속성에서만 작동하기 때문에 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
내가 실수하지 않으면 observeValueForKeyPath 호출 접근 방식은 Swift2입니다.
Fattie

9

예.

KVO에는 동적 디스패치가 필요하므로 dynamic메소드, 속성, 첨자 또는 이니셜 라이저에 수정자를 추가하기 만하면됩니다 .

dynamic var foo = 0

dynamic선언에 대한 참조 동적를 통해 전송 및 액세스 할 것이라는 점을 수정 보장하지만 objc_msgSend.


7

Rob의 답변 외에도. 이 클래스는에서 상속해야하며 NSObject속성 변경을 트리거하는 3 가지 방법이 있습니다.

사용 setValue(value: AnyObject?, forKey key: String)에서NSKeyValueCoding

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

사용 willChangeValueForKeydidChangeValueForKey에서NSKeyValueObserving

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

사용하십시오 dynamic. 스위프트 타입 호환성 참조

동적 수정자를 사용하여 메소드의 구현을 동적으로 대체하는 키-값 관찰과 같은 API를 사용하는 경우 Objective-C 런타임을 통해 멤버에 대한 액세스를 동적으로 디스패치하도록 요구할 수 있습니다.

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

그리고 getter 및 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사용하지 않고 NSObjectObjective-C

가용성 : iOS 13.0+ , macOS 10.15+, tvOS 13.0+, watchOS 6.0+, Mac Catalyst 13.0+,Xcode 11.0+

참고 : 값 유형이 아닌 클래스에만 사용해야합니다.

암호:

스위프트 버전 : 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의 기본 부분이므로 향후 추가 될 가능성이 높습니다. 현재 문서는 이것을 암시하는 것으로 보입니다.

키-값 관찰

앞으로의 정보.

Cocoa 및 Objective-C와 함께 Swift 사용


2
분명히, 당신이 참조하는 가이드는 이제 Swift에서 KVO를 수행하는 방법을 설명합니다.
Rob

4
Yep, 2014 년 9 월 현재 구현
Max MacLeod

4

한 가지 중요한 점은 Xcode7 베타로 업데이트 한 후 "메소드는 슈퍼 클래스의 메소드를 대체하지 않습니다"라는 메시지가 표시 될 수 있습니다 . 그것은 논증의 선택성 때문입니다. 관찰 처리기가 다음과 정확히 같은지 확인하십시오.

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

2
Xcode 베타 6에서는 다음이 필요합니다. override 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와 같은 유형에 문제가있는 사람을위한 또 다른 예? 그리고 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.