Swift extension event listener on property - ios

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'")

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.

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.

Using property observers on NSManaged vars

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)

KVO observation not working with Swift generics

If I observe a property using KVO, if the observer is a generic class then I receive the following error:
An -observeValueForKeyPath:ofObject:change:context: message was
received but not handled.
The following setup demonstrates the problem succinctly. Define some simple classes:
var context = "SomeContextString"
class Publisher : NSObject {
dynamic var observeMeString:String = "Initially this value"
}
class Subscriber<T> : NSObject {
override func observeValueForKeyPath(keyPath: String,
ofObject object: AnyObject,
change: [NSObject : AnyObject],
context: UnsafeMutablePointer<Void>) {
println("Hey I saw something change")
}
}
Instantiate them and try to observe the publisher with the subscriber, like so (done here inside a UIViewController subclass of a blank project):
var pub = Publisher()
var sub = Subscriber<String>()
override func viewDidLoad() {
super.viewDidLoad()
pub.addObserver(sub, forKeyPath: "observeMeString", options: .New, context: &context)
pub.observeMeString = "Now this value"
}
If I remove the generic type T from the class definition then everything works fine, but otherwise I get the "received but not handled error". Am I missing something obvious here? Is there something else I need to do, or are generics not supposed to work with KVO?
Explanation
There are two reasons, in general, that can prevent a particular Swift class or method from being used in Objective-C.
The first is that a pure Swift class uses C++-style vtable dispatch, which is not understood by Objective-C. This can be overcome in most cases by using the dynamic keyword, as you obviously understand.
The second is that as soon as generics are introduced, Objective-C looses the ability to see any methods of the generic class until it reaches a point in the inheritance hierarchy where an ancestor is not generic. This includes new methods introduced as well as overrides.
class Watusi : NSObject {
dynamic func watusi() {
println("watusi")
}
}
class Nguni<T> : Watusi {
override func watusi() {
println("nguni")
}
}
var nguni = Nguni<Int>();
When passed to Objective-C, it regards our nguni variable effectively as an instance of Watusi, not an instance of Nguni<Int>, which it does not understand at all. Passed an nguni, Objective-C will print "watusi" (instead of "nguni") when the watusi method is called. (I say "effectively" because if you try this and print the name of the class in Obj-C, it shows _TtC7Divided5Nguni00007FB5E2419A20, where Divided is the name of my Swift module. So ObjC is certainly "aware" that this is not a Watusi.)
Workaround
A workaround is to use a thunk that hides the generic type parameter. My implementation differs from yours in that the generic parameter represents the class being observed, not the type of the key. This should be regarded as one step above pseudocode and is not well fleshed out (or well thought out) beyond what's needed to get you the gist. (However, I did test it.)
class Subscriber : NSObject {
private let _observe : (String, AnyObject, [NSObject: AnyObject], UnsafeMutablePointer<Void>) -> Void
required init<T: NSObject>(obj: T, observe: ((T, String) -> Void)) {
_observe = { keyPath, obj, changes, context in
observe(obj as T, keyPath)
}
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
_observe(keyPath, object, change, context)
}
}
class Publisher: NSObject {
dynamic var string: String = nil
}
let publisher = Publisher()
let subscriber = Subscriber(publisher) { _, _ in println("Something changed!") }
publisher.addObserver(subscriber, forKeyPath: "string", options: .New, context: nil)
publisher.string = "Something else!"
This works because Subscriber itself is not generic, only its init method. Closures are used to "hide" the generic type parameter from Objective-C.

Resources