KVO on singleton property? - ios

Hi all I'm trying to implement KVO on one of the string properties within a Singleton class. I'm currently running into some errors when trying to add an observer and was hoping that someone could tell me what I'm doing wrong.
The below shows my singleton class.
class User: NSObject {
static let currentUser = User()
private override init() {}
var pictureString: String?
}
In a separate ViewController's viewDidLoad, I've tried to add an observer to the pictureString variable like so.
self.addObserver(self, forKeyPath: #keyPath(User.currentUser.pictureString), options: [.old, .new, .initial], context: nil)
the problem I'm dealing with right now is that it is stating that currentUser is not KVC-compliant. I'm currently getting the below error.
addObserver: forKeyPath:#"currentUser.pictureString" options:7 context:0x0] was sent to an object that is not KVC-compliant for the "currentUser" property.'
I'd appreciate any help thanks.

See this answer:
As for now, Swift cannot have observable class properties.
If you really want to use a singleton, you could convert it to a non-static singleton with a normal property, which then also can be KVO'd... something like this:
class UserManager: NSObject {
private override init() {}
static var instance = UserManager()
var currentUser = User()
}
class User: NSObject {
dynamic var pictureString: String?
}
Note the dynamic keyword in the variable declaration -- this is needed to make KVO work in Swift (dynamic dispatch is explained here).
Then in your VC's viewDidLoad you can register for changes with this:
UserManager.instance.addObserver(self, forKeyPath: #keyPath(UserManager.currentUser.pictureString), options: [.old, .new, .initial], context: nil)

class Viewcontroller: UIViewController {
let user = User.currentUser
override viewDidLoad() {
super.viewDidLoad()
addObserver(self, forKeyPath: #keyPath(user.pictureString), options: [.old, .new], context: nil)
}
}
You can declare your singleton and use that for #keyPath. Otherwise, you will raise an exception.

Related

swift UITableView/ UICollectionView call observe(\.xxx) didn't work

collection.addObserver(self, forKeyPath: #keyPath(UICollectionView.contentSize), options: .new, context: nil)
collection.observe(\.contentSize) { (collection, change) in
}
when I use "addObserver" to observe contentSize, it's worked,
but observe(.contentSize) is not work, I don't know why.
Compare with the example here:
class MyObserver: NSObject {
#objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?
init(object: MyObjectToObserve) {
objectToObserve = object
super.init()
observation = observe(
\.objectToObserve.myDate,
options: [.old, .new]
) { object, change in
print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}
}
}
The observe method returns an "observation" token, and you need to hang on to it. It stops observing if/when it goes out of scope.

How to instantiate an object that depends on self in the init function?

I understand that all properties need to be defined before calling super.init(). But what if the initialization of a property depends on self? In my case I need to initialize an object that has a delegate, which I need to set to self. What is the best way to do this?
class MyClass : NSObject {
var centralManager : CBCentralManager
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
}
This is wrong, because centralManager is not initialized before super.init. But I can't change the order either, because then I would be using self before super.init.
The problem
Let's say CBCentralManager is defined as follow
protocol CBCentralManagerDelegate { }
class CBCentralManager {
init(delegate: CBCentralManagerDelegate, queue: Any?) {
}
}
The solution
This is how you should define your class
class MyClass: CBCentralManagerDelegate {
lazy var centralManager: CBCentralManager = {
[unowned self] in CBCentralManager(delegate: self, queue: nil)
}()
}
How does it work?
As you can see I am using a lazy property to populate the centralManager property.
The lazy property has an associated closure that is executed the first time the lazy property is read.
Since you can read the lazy property only after the current object has been initialised, everything will work fine.
Where's NSObject?
As you can see I removed the inheritance of MyClass from NSObject. Unless you have a very good reason to inherit from NSObject well... don't do it :)

Observing a value of a static var in a class?

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

Observe property change of class instance

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.

Swift Reference Variable from Specific Instance of Different Class

I'm new to Swift and iOS in general. I'm using Swift to write an app. This app has two files, ViewController.swift and BTService.swift.
ViewController.swift has a class ViewController of type UIViewController, and BTService.swift has a class BTService of types NSObject and CBPeripheralDelegate. I have a slider set up in the UIViewController class, and its value is assigned to variable currentValue.
Now, I want to be able to reference currentValue from within the BTService class. How can I go about doing this? I've noticed that if I define a variable, test, in the file ViewController before the class UIViewController, that I can reference test in BTService. But that's of no use to me since I can't (to my knowledge) get the slider value to be assigned to test unless test is defined within the UIViewController class.
Here's my ViewController and the currentValue variable definition.
class ViewController: UIViewController {
#IBOutlet var positionLabel: UILabel!
#IBOutlet var positionSlider: UISlider!
#IBOutlet var connectionLabel: UILabel!
#IBAction func sliderValChanged(sender: UISlider) {
var currentValue = Float(positionSlider.value)
Any help would be greatly appreciated! Thanks.
You can use NSUserDefaults to store data within your application and share it between view controllers.
In the example you give, you could store it like this:
let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
defaults.setFloat(Float(positionSlider.value), forKey: "sliderValue") // Write to NSUserDefaults
defaults.synchronize()
Then, whenever you need access to it in another file (or in this one), you can just call floatForKey:
if let currentValue: Float = defaults.floatForKey("sliderValue") {
// Access slider value
} else {
// Probably not saved
}
Is your BTService a property within the UIViewController?
Could you use Key Value Observing (KVO)
Inside your UIViewController
override func viewDidLoad() {
self.addObserver(self, forKeyPath: "positionSlider.value", options: .New, context: nil)
}
deinit() {
self.removeObserver(self, forKeyPath: "positionSlider.value")
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "positionSlider.value" {
// update service property
}
}

Resources