Given an instance of a class, how can I observe a property change?
For e.g., I'm building an SDK that initializes a host app's chat view to provide more functionality with a simple inplementation that looks like:
sdk.initialize(chatView)
In that initializing function, I need to track the host app's chat-view's hidden property so that the SDK's view matches.
A simple KVO example for observing hidden:
class SDKViewController : UIViewController {
private var context = 0
private var observingView: UIView?
func initialize(view: UIView) {
removeObservations()
observingView = view
// start observing changes to hidden property of UIView
observingView?.addObserver(self, forKeyPath: "hidden", options: [.New], context: &context)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let newValue = change?[NSKeyValueChangeNewKey] as? Bool where context == &self.context {
print("hidden changed: \(newValue)")
}
}
// this is called by deinit
// it should also be called if they can deregister the view from your SDK
func removeObservations() {
if let view = observingView {
view.removeObserver(self, forKeyPath: "hidden")
observingView = nil
}
}
deinit {
removeObservations()
}
}
This is making some assumptions about your configuration, but if you allow initialization of many views, you can adjust easily.
Also, a lot of this is more concise if you use KVOController by Facebook, which is not in Swift.
Edit: Just to note, hidden does work with KVO.
Edit #2: Updated YourSDKClass to SDKViewController (NSObject -> UIViewController)
Here is an example using protocols
protocol MyClassDelegate:class {
func myClassValueDidChange(newValue:Int)
}
class MyClass {
weak var delegate:MyClassDelegate?
var value = 0 {
didSet {
delegate?.myClassValueDidChange(value)
}
}
}
class ViewController:UIViewController,MyClassDelegate {
let myClass = MyClass()
override func viewDidLoad() {
super.viewDidLoad()
myClass.delegate = self
}
func myClassValueDidChange(newValue:Int) {
//Do something
}
}
You can use Key Value Observing (KVO) to monitor changes to general properties on classes, which includes the hidden property on UIView instances. This is done using addObserver:forKeyPath:options:context: defined in the NSKeyValueObserving protocol.
Note that you can also hide a view by removing it from its superview or by setting its alpha to zero.
Related
I am trying to implement an observer for changes to a value for a give key in UserDefaults from the ios native part of a multiplatform project written in Kotlin/Native. Here is the code that I wrote:
fun subscribeForDataChange(storeName: String, callback: () -> Unit) {
NSUserDefaults(storeName).addObserver(
object : NSObject() {
fun observeValue(
observer: NSObject,
forKeyPath: String,
options: NSKeyValueObservingOptions,
context: COpaquePointer?
) {
callback()
print("Data Changed!!!")
}
},
options = NSKeyValueObservingOptionNew,
forKeyPath = DATA_KEY,
context = null
)
}
The problem is that I never get a notification, most probably because the observeValue is not defined in NSObject, but what else should I do to achieve that?
Are you looking for an NSObject change, or you want to observe NSUserDefaults? If the latter, check out Multiplatform Settings. Here's the code that wires up observers.
https://github.com/russhwolf/multiplatform-settings/blob/master/multiplatform-settings/src/appleMain/kotlin/com/russhwolf/settings/AppleSettings.kt
Here is the solution for 2 apps in the same group sharing UserDefaults. I share SQLite database between two processes and I need to know when one process writes somethink to db. Classical flows are not triggered so I wrote a flow helper, which emit values in Kotlin when NSUserDefaults changes.
Implement NSObject as a part of the Swift codebase (Swift code inspiration). Swift calls a Kotlin method when NSUserDefaults changes. Firstly define interfaces.
interface NSUserDefaultsKotlinHelper {
fun userDefaultsChanged()
}
interface SwiftInjector {
fun injectIntoSwift(nsUserDefaultsKotlinHelper: NSUserDefaultsKotlinHelper?)
}
Let that interface inject listener into Swift code :
class InterprocessObserver: NSObject, SwiftInjector {
let key: String = "interprocess_communication"
private var nsUserDefaultsKotlinHelper : NSUserDefaultsKotlinHelper?
private let userDefaults = UserDefaults.init(suiteName: "group.your.group.id")
override init() {
super.init()
userDefaults?.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil)
}
func injectIntoSwift(nsUserDefaultsKotlinHelper: NSUserDefaultsKotlinHelper?) {
self.nsUserDefaultsKotlinHelper = nsUserDefaultsKotlinHelper
}
func dataChangedFromAnotherProcess(data : [AnyHashable : Any]) {
userDefaults?.set(data, forKey: key)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let _ = change, object != nil, keyPath == key else { return }
nsUserDefaultsKotlinHelper?.userDefaultsChanged()
}
deinit {
userDefaults?.removeObserver(self, forKeyPath: key, context: nil)
}
}
Inject listener in Kotlin - I will inject when a flow starts to collect:
class InterProcessCommunication(val interPlatformInjector: InterplatformInjector) : InterplatformInjector by interplatformInjector {
val testFlow: Flow<Emitter> = flow {
val channel = Channel<Emitter>(CONFLATED)
channel.trySend(Emitter.STAY_CALM)
val listener = object : IInterprocessCommunication {
override fun interProcessChanged() {
channel.trySend(Emitter.EMIT)
}
}
interPlatformInjector.injectListener(listener)
try {
for (item in channel) {
emit(item)
}
} finally {
interPlatformInjector.injectListener(null)
}
}
}
Objects creation with Koin would be:
//Swift
func initObservers() {
let interplatformInjector = InterprocessObserver()
initKoin(interplatformInjector : interplatformInjector)
}
//Kotlin
fun initKoin(interplatformInjector : InterplatformInjector){
startKoin {
module {
single {InterProcessCommunication(interplatformInjector)}
}
}
}
//Swift Second process (for example NotificationService)
func dataChanged(interprocessObserver : InterprocessObserver) {
interprocessObserver.dataChangedFromAnotherProcess(data) //data could be anythink - for example a string
}
The method dataChenged() will trigger a Kotlin flow. Is this what you are looking for?
I have a class with a static var where the current online connection status is stored. I want to observe the value of ConnectionManager.online through other classes. I wanted to do this with KVO, but declaring a static variable as dynamic causes an error:
class ConnectionManager: NSObject {
dynamic static var online = false
// adding 'dynamic' declaration causes error:
// "A declaration cannot be both 'final' and 'dynamic'
}
What is a most elegant way of doing this?
Update. This my code for the KVO part:
override func viewDidLoad() {
super.viewDidLoad()
ConnectionManager.addObserver(
self,
forKeyPath: "online",
options: NSKeyValueObservingOptions(),
context: nil
)
}
override func observeValueForKeyPath(keyPath: String?,
ofObject object: AnyObject?,
change: [String : AnyObject]?,
context: UnsafeMutablePointer<Void>) {
if keyPath == "online" {
print("online status changed to: \(ConnectionManager.online)")
// doesn't get printed on value changes
}
}
As for now, Swift cannot have observable class properties. (In fact, static properties are just global variables with its namespace confined in a class.)
If you want to use KVO, create a shared instance (singleton class) which has online property and add observer to the instance.
I solved it with the singleton pattern suggested by #OOper.
class ConnectionManager: NSObject {
static let sharedInstance = ConnectionManager()
private override init() {} // This prevents others from using the default '()' initializer for this class.
#objc dynamic var online = false
}
Then:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.tableFooterView = UIView()
ConnectionManager.sharedInstance.addObserver(self,
forKeyPath: "online",
options: [.new, .initial],
context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if object is ConnectionManager && keyPath == "online" {
// ...
}
}
Try replacing dynamic static var online = false to #nonobjc static var online = false
What's happening is that because it inherits from NSObject, Swift is trying to generate getters and setters for it. Because you are creating it in swift, using the #nonobjc attribute solves the problem.
EDIT:
I don't believe you can observe static variables through KVO because of how it works
Here is a link and snippet from Apple's Guide on KVO
Unlike notifications that use NSNotificationCenter, there is no
central object that provides change notification for all observers.
Instead, notifications are sent directly to the observing objects when
changes are made.
Perhaps, instead of using KVO, you could declare online like:
static var online = false {
didSet{
//code to post notification through regular notification center
}
}
If you're set on using it, this question might point you towards the right direction — it'll involve diving deeper into how KVO works: Is it possible to set up KVO notifications for static variables in objective C?
I would suggest property wrapper, I tried the example below and worked perfectly for me:
#propertyWrapper
struct StaticObserver<T> {
private var value:T
init(value:T) {
self.value = value
}
var wrappedValue: T {
get {
// Do your thing
return self.value
}
set {
// Do your thing before set
self.value = newValue
// Do your thing after set
}
}
#StaticObserver(value: false)
dynamic static var online:Bool
I was wondering is it possible to edit properties in Swift through and extension?
I want to do something like this.
extension UIGestureRecognizer {
var state: UIGestureRecognizerState {
didSet(state) {
self.stateChanged(state)
}
}
You cannot change the implementation of UIGestureRecognizer's state property. If you could publicly get and set state, then you could create a different computed property myState that forwarded get and set to state. Unfortunately you can't. You can however get around this using KVO (Key Value Observing).
First off, we create an object that can respond to KVO notifications for our gesture and implement observeValueForKeyPath which is called when the state property is called.
class StateObserver : NSObject {
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let gesture = object as? UIGestureRecognizer {
gesture.stateChanged(gesture.state)
}
}
}
Second, register a StateObserver instance to get notified when tap.state is changed.
let stateObserver = StateObserver()
let tap = UITapGestureRecognizer()
tap.addObserver(stateObserver, forKeyPath: "state", options: [.New, .Old], context: nil)
Third, don't forget to unregister StateObserver once you're done.
tap.removeObserver(stateObserver, forKeyPath: "state'")
Is there a way to get notified when there is a change in a UILabel's text or would I be better off using a UITextField with userInteractionEnabled set to false and using its UIControlEditingChanged event to fulfil my purpose?
For ex. I need to run certain lines of code every time I change the UILabel's text and accordingly. So instead of writing those 100 lines of almost similar code for every case I change the UILabel's text, I wish to write it together in one place and call it every time the UILabel is changed. I don't even know if that makes any sense. Forgive me but I cannot expose much of the code.
Create a class that inherits from UILabel. Such as:
class YourLabel: UILabel {
override var text: String? {
didSet {
if let text = text {
println("Text changed.")
} else {
println("Text not changed.")
}
}
}
}
Create a outlet of this class for your object.
#IBOutlet weak var label: YourLabel!
label.text = "abc"
Swift 3
First, add an observer to UILabel for key path text.
label.addObserver(self, forKeyPath: "text", options: [.old, .new], context: nil)
Then
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "text" {
print("old:", change?[.oldKey])
print("new:", change?[.newKey])
}
}
Swift 2
label.addObserver(self, forKeyPath: "text", options: [.Old, .New], context: nil)
Then
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "text" {
print("old:", change["old"])
print("new:", change["new"])
}
}
Console Output
For example console would be:
old: Optional(<null>)
new: Optional(ABC)
You can do this simply with a subclass of UILabel:
class Label : UILabel {
override var text: String? {
didSet {
print("Text changed from \(oldValue) to \(text)")
}
}
}
oldValue is a special value provided by Swift.
See Property Observers
For Swift 3.2 and later, the preferred method is to use a closure-based observer:
#IBOutlet public weak var label: UILabel!
var textObserver: NSKeyValueObservation?
func someAppropriateFunction() {
...
textObserver = label.observe(\.text) { [weak self] (label, observedChange) in
self?.updateStuff()
}
}
The closure will pass you the label instance and an NSKeyValueObservedChange that includes the following properties:
indexes: IndexSet?
isPrior: Bool
kind: NSKeyValueObservedChange<Value>.Kind
newValue: Value?
oldValue: Value?
Swift 5
If you want to observe text changes in the label and make an animation. You need to create subclass UILabel
class ObservedLabelAnimate: SpringLabel {
override var text: String? {
didSet {
if let text = text {
if oldValue != text {
animation = "fadeInLeft"
duration = 1.2
animate()
}
print("This is oldvalue \(oldValue), and this is the new one \(text)")
}
}
}
}
for this example, I subclass 'SpringLabel' that inherits from UILabel
from https://github.com/MengTo/Spring
Basically it will check if the oldValue and text (new one) is different then the animation will be fired
To use:
#IBOutlet weak var label: ObservedLabelAnimate!
I have a var declared in a class like so:
#NSManaged var isFavorite: Bool
I would like to declare a property observer, very similar to the one below.
var organization: String {
didSet { postNotificationWithName( "newData" ) }
}
However, Swift tells me that having property observers on NSManaged vars is not allowed. Is there any way I can implement such a feature or something similar for my isFavorite variable?
Yes-- delete the #NSManaged. It's not absolutely required, but if you delete it you unfortunately need to implement get and set for the property. You would need to add something like
The #objc is only needed if you want to be able to do KVO on the property.
#objc public var newData: String? {
set {
willChangeValue(forKey: "newData")
setPrimitiveValue(newValue, forKey: "newData")
didChangeValue(forKey: "newData")
}
get {
willAccessValue(forKey: "newData")
let text = primitiveValue(forKey: "newData") as? String
didAccessValue(forKey: "newData")
return text
}
}
It's kind of annoying to implement both of these if you don't actually need them but that's the way it is for now.
Since you'll have a set, you might not need a didSet, but you can still add a didSet if you want one.
Whoops! Paul Patterson is right. What you're supposed to use is Key Value Observing - which is exactly what it says you're supposed to do in the link I suggested.
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html
See also swift notes:
https://developer.apple.com/library/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html
(use the 'On This Page' menu at the top right of the page for Key-Value Observing)
So something like
objectToObserve.addObserver(self, forKeyPath: "organization", options: .New, context: &myContext)
paired with
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
Override NSManagedObject's func didChangeValue(forKey key: String) see (https://developer.apple.com/documentation/coredata/nsmanagedobject/1506976-didchangevalue)