The framework to enhance KVO usage in Swift.
- Array KVO
- Affecting property wrapper
- AddObserver with auto removing (obj-c runtime only)
You can add this project as a submodule and incorporate KVOMagic.xcodeproj.
// TODO
- Using Xcode go to File > Swift Packages > Add Package Dependency
- Paste the project URL: https://github.com/radulov/KVOMagic.git
- Click on next and select the project target:
- KVOMagic
If you have doubts, please, check the following links:
After successfully retrieved the package and added it to your project, just import KVOMagic
and you can get the full benefits of it.
Grab last artifacts from GithubActions or from Release page
You can add an observer for every object in an array by using a special keyPath prefix - "$".
Typical usage: .arrayKVO + #keyPath(arrayOwner.arrayProperty.propertyOfObjectInArray)
For example you have such structure:
class MyObject: NSObject {
@objc dynamic var intProperty: Int
}
class MyObjectsCollection: ArrayOwner {
@objc dynamic var array: [MyObject]
}
You can add an observer for every object in array by one line:
let collection = MyObjectsCollection()
collection.startObserving(.arrayKVO + #keyPath(MyObjectsCollection.array.intProperty)) { _, _ in
\\ Do some stuff here
}
So whenever intProperty
will be changed, you will receive a callback.
For everything to work, you need to subclass ArrayOwner
and make sure all your properties support objc runtime.
Array KVO supports all kinds of usage, such as:
- KeyPathForValuesAffecting
- Mutable array
- Affecting property wrapper
- Nested observing
Property wrapper for encapsulating KeyPathForValuesAffecting()
and support for swift KeyPath.
It will also cache computed value and change it only on new KVO notifications.
Classical example:
class Contact: WrapperOwner {
@objc dynamic var name: String?
@objc dynamic var surname: String?
@Computed2({ $0 + " " + $1 }, self, \.name, \.surname) @objc dynamic var fullname
}
There are four flavours:
@Computed1({ $0 }, self, \.keyPath) @objc dynamic var name
@Computed2({ $0, $1 }, self, \.keyPath1, \.keyPath2) @objc dynamic var name
@Computed3({ $0, $1, $2 }, self, \.keyPath1, \.keyPath2, \.keyPath3) @objc dynamic var name
These three are type-safe and use Swift KeyPath. Every time value for any of provided properties will change, the computing block will be called with an appropriate count of arguments.
@Computed({ `self` in }, self, #keyPath("stringKeypath")) @objc dynamic var name
This one is using String keyPath and will pass self
as an argument to "computing block". This property wrapper can accept various count of affecting keyPaths.
New observe()
function analogue called startObserving()
.
The key differences are:
- No need for storing an observer object. An observer will remove automatically in
deinit()
. - Fixed self-observing crash. You can safely observe properties from the
self
object. - Fixed removing observer in
.initial
callback. You can safely callstopObserving()
even in the first callback called immediately afterstartObserving()
There are two flavours:
func startObserving<Value>(_ keyPath: KeyPath<Self, Value>, _ owner: Any? = nil, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (Self, KeyValueObservedChange<Value>) -> Void) {}
Typesafe variant with swift KeyPath.
func startObserving(_ keyPath: String, _ owner: Any? = nil, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (Self, KeyValueObservedChange<Any>) -> Void)
Obj-c equivalent with string keyPath.
Copy content of the Code snippets folder to ‘~/Library/Developer/Xcode/UserData/CodeSnippets’.
Use @ObservableArray
property wrapper to send notifications from owner for every change in any object in array.
Example:
class Contact: ObservableObject {
@Published var name: String?
@Published var surname: String?
}
class ContactsOwner: ObservableObject {
@ObservableArray var contacts = [Contact]()
}
let owner = ContactsOwner()
owner.contacts[2].name = "test" // This will send notification for `owner`
Note, owner and objects in array should conforms to ObservableObject
Use @FromArray
property wrapper. It's syntax similar to @ComputedX
.
Example:
class Contact: ObservableObject {
@Published var name: String?
}
class ContactsOwner: PureWrapperOwner {
@ObservableArray var contacts = [Contact]()
@FromArray({ contacts in contacts.first { $0.name == "John" }}, /ContactsOwner.$contacts) var anyJohn
}
let owner = ContactsOwner()
owner.contacts[2].name = "John" // This will send notification for `anyJohn`
Note:
- owner should conforms to
WrapperOwnerProtocol
orPureWrapperOwner
- objects in array should conforms to
ObservableObject
- array should use
@ObservableArray
Check unit tests for the samples.
- Pull this repository.
- Change Apple ID account in Signing&Capabilities;
- Specific macOS and Xcode versions (see KVOMagic.xcodeproj, Xcode 12.4, and MacOS 10.15.7 for now).
Every version number is described as:
x.y.z
Where:
- x – major version;
- y – minor version;
- z – bugfix version.