Is it possible to observe iOS NSObject value changes with Kotlin/Native - ios

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?

Related

why do we need the service class using singleton in Swift?

I watch realm tutorial from youtube, the instructor using singleton in the service class like the code below :
import Foundation
import RealmSwift
class RealmService {
// singleton
private init() {}
static let shared = RealmService()
var realm = try! Realm()
func save<T: Object>(object: T) {
do {
try realm.write {
realm.add(object)
}
} catch {
post(error)
}
}
func update<T: Object>(object: T, for dictionary: [String: Any?]) {
do {
try realm.write {
for (key,value) in dictionary {
object.setValue(value, forKey: key)
}
}
} catch {
post(error)
}
}
func delete<T: Object>(object: T) {
do {
try realm.write {
realm.delete(object)
}
} catch {
post(error)
}
}
func post(_ error: Error) {
NotificationCenter.default.post(name: Notification.Name.realmError, object: error)
}
func observerRealmErrors(in vc: UIViewController, completion: #escaping(Error?) -> Void ) {
NotificationCenter.default.addObserver(forName: Notification.Name.realmError, object: nil, queue: nil) { (notification) in
completion(notification.object as? Error)
}
}
func stopObservingErrors(in vc: UIViewController) {
NotificationCenter.default.removeObserver(vc, name: NSNotification.Name.realmError, object: nil)
}
}
I understand that A singleton is an object which is instantiated exactly once. but why it needs to use singleton for this purpose? if the code s like below, I understand, because it doesn't make any sense if there is more than one "bobby" as the ID, then it makes sense to create a singleton class
but how about that realm service like the code above? why it needs to use singleton? I have seen some instructor using singleton for the service class
class AccountManager {
static let sharedInstance = AccountManager()
var userInfo = (ID: "Bobby", Password: 01036343984)
// Networking: communicating server
func network() {
// get everything
}
private init() { }
}
You gave an example of a shared property (userInfo) in your second block , but in the first block the instructor uses a singleton to prevent himself from duplicating code everywhere inside the project , and only to have one single unit to do the crud ( create , read , update , delete) operations of the Realm database and it's another use of a singleton class other than sharing a value
It's simple. If you don't try to using a singleton in realm case like you have wrote, you should call realm = try! Realm() method every time when you trying to use realm objects or functions(saving deleting etc..). It will make your code too boring and also can causes many miss point. If you using realm with the singleton code like you have wrote(it wrote with generic functions), you can save your object in realm database with just single line of code like RealmService.save(yourobject).

KVO infinit loop with 2 way binding

I have two classes. Class A has Class B, but class B does not know of the existence of class A
Both classes can be altered by external factors (such as services or logic)
But I need to keep both classes synchronized with the same value
As class A knows of class B, I can do a direct assignment after its value is changed
To make the class B know the Class A, I decided to implement KVO, in this way class A is notified when class B is changed
My code looks something like this
class A : NSObject {
var b : B
#objc dynamic var anOtherString:String? {
didSet{
b.someString = self.anOtherString
}
}
override init() {
self.b = B()
super.init()
addObserver(self, forKeyPath: #keyPath(b.someString), options: [.old , .new], context: nil)
}
// MARK: - Key-Value Observing
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(b.someString) {
// Update Time Label
anOtherString = b.someString
}
}
}
class B : NSObject {
#objc dynamic var someString: String?
}
The problem is that my code stays in infinite loop
Because whenever my class B is changed it notifies class A, and when class A is updated it changes again the value of class B that creates a new notification and so on...
I already tried to analyze the Thread.callStackSymbols to detect the cycle and stop it but without success.
Since you cannot allow A to update B when A was changed due to B, you could have a boolean variable that tracks if A needs to update B.
class A : NSObject {
var b : B
var bChanged: Bool = false // Logic to track if B made a change
#objc dynamic var anOtherString:String? {
didSet{
// If B did not make a change, then update B
if !bChanged {
b.someString = self.anOtherString
// Otherwise set the flag
}else {
bChanged = false
}
}
}
override init() {
self.b = B()
super.init()
addObserver(self, forKeyPath: #keyPath(b.someString), options: [.old , .new], context: nil)
}
// MARK: - Key-Value Observing
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(b.someString) {
// Update Time Label
bChanged = true // B made a change
anOtherString = b.someString
}
}
}

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.

How do I register UndoManager in Swift?

How do I use UndoManager (previously NSUndoManager) in Swift?
Here's an Objective-C example I've tried to replicate:
[[undoManager prepareWithInvocationTarget:self] myArgumentlessMethod];
Swift, however, seems to not have NSInvocation, which (seemingly) means I can't call methods on the undoManager that it doesn't implement.
I've tried the object-based version in Swift, but it seems to crash my Playground:
undoManager.registerUndoWithTarget(self, selector: Selector("myMethod"), object: nil)
However it seems to crash, even with my object accepts an argument of type AnyObject?
What's the best way to do this in Swift? Is there a way to avoid sending an unnecessary object with the object-based registration?
OS X 10.11+ / iOS 9+ Update
(Works the same in Swift 3 as well)
OS X 10.11 and iOS 9 introduce a new NSUndoManager function:
public func registerUndoWithTarget<TargetType>(target: TargetType, handler: TargetType -> ())
Example
Imagine a view controller (self in this example, of type MyViewController) and a Person model object with a stored property name.
func setName(name: String, forPerson person: Person) {
// Register undo
undoManager?.registerUndoWithTarget(self, handler: { [oldName = person.name] (MyViewController) -> (target) in
target.setName(oldName, forPerson: person)
})
// Perform change
person.name = name
// ...
}
Caveat
If you're finding your undo isn't (ie, it executes but nothing appears to have happened, as if the undo operation ran but it's still showing the value you wanted to undo from), consider carefully what the value (the old name in the example above) actually is at the time the undo handler closure is executed.
Any old values to which you want to revert (like oldName in this example) must be captured as such in a capture list. That is, if the closure's single line in the example above were instead:
target.setName(person.name, forPerson: person)
...undo wouldn't work because by the time the undo handler closure is executed, person.name is set to the new name, which means when the user performs an undo, your app (in the simple case above) appears to do nothing since it's setting the name to its current value, which of course isn't undoing anything.
The capture list ([oldName = person.name]) ahead of the signature ((MyViewController) -> ()) declares oldName to reference person.name as it is when the closure is declared, not when it's executed.
More Information About Capture Lists
For more information about capture lists, there's a great article by Erica Sadun titled Swift: Capturing references in closures. It's also worth paying attention to the retain cycle issues she mentions. Also, though she doesn't mention it in her article, inline declaration in the capture list as I use it above comes from the Expressions section of the Swift Programming Language book for Swift 2.0.
Other Ways
Of course, a more verbose way to do it would be to let oldName = person.name ahead of your call to registerUndoWithTarget(_:handler:), then oldName is automatically captured in scope. I find the capture list approach easier to read, since it's right there with the handler.
I also completely failed to get registerWithInvocationTarget() to play nice with non-NSObject types (like a Swift enum) as arguments. In the latter case, remember that not only should the invocation target inherit from NSObject, but the arguments to the function you call on that invocation target should as well. Or at least be types that bridge to Cocoa types (like String and NSString or Int and NSNumber, etc.). But there were also problems with the invocation target not being retained that I just couldn't solve. Besides, using a closure as a completion handler is far more Swiftly.
In Closing (Get it?)
Figuring all this out took me several hours of barely-controlled rage (and probably some concern on the part of my Apple Watch about my heart rate - "tap-tap! dude... been listening to your heart and you might want to meditate or something"). I hope my pain and sacrifice helps. :-)
Update 2: Swift in Xcode 6.1 has made undoManager an optional so you call prepareWithInvocationTarget() like this:
undoManager?.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index)
Update: Swift in Xcode6 beta5 simplified use of undo manager's prepareWithInvocationTarget().
undoManager.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index)
Below was what was needed in beta4:
The NSInvocation based undo manager API can still be used, although it wasn't obvious at first how to call it. I worked out how to call it successfully using the following:
let undoTarget = undoManager.prepareWithInvocationTarget(myTarget) as MyTargetClass?
undoTarget?.insertSomething(someObject, atIndex: index)
Specifically, you need to cast the result of prepareWithInvocationTarget() to the target type, although remember to make it optional or you get a crash (on beta4 anyway). Then you can call your typed optional with the invocation you want to record on the undo stack.
Also make sure your invocation target type inherits from NSObject.
I tried this in a Playground and it works flawlessly:
class UndoResponder: NSObject {
#objc func myMethod() {
print("Undone")
}
}
var undoResponder = UndoResponder()
var undoManager = UndoManager()
undoManager.registerUndo(withTarget: undoResponder, selector: #selector(UndoResponder.myMethod), object: nil)
undoManager.undo()
I tried for 2 days to get Joshua Nozzi's answer to work in Swift 3, but no matter what I did the values were not captured.
See: NSUndoManager: capturing reference types possible?
I gave up and just managed it myself by keeping track of changes in undo and redo stacks. So, given a person object I would do something like
protocol Undoable {
func undo()
func redo()
}
class Person: Undoable {
var name: String {
willSet {
self.undoStack.append(self.name)
}
}
var undoStack: [String] = []
var redoStack: [String] = []
init(name: String) {
self.name = name
}
func undo() {
if self.undoStack.isEmpty { return }
self.redoStack.append(self.name)
self.name = self.undoStack.removeLast()
}
func redo() {
if self.redoStack.isEmpty { return }
self.undoStack.append(self.name)
self.name = self.redoStack.removeLast()
}
}
Then to call it, I don't worry about passing arguments or capturing values since the undo/redo state is managed by the object itself. So say you have a ViewController that is managing your Person objects, you just call registerUndo and pass nil
undoManager?.registerUndo(withTarget: self, selector:#selector(undo), object: nil)
I think it would be Swiftiest if NSUndoManager accepted a closure as an undo registration. This extension will help:
private class SwiftUndoPerformer: NSObject {
let closure: Void -> Void
init(closure: Void -> Void) {
self.closure = closure
}
#objc func performWithSelf(retainedSelf: SwiftUndoPerformer) {
closure()
}
}
extension NSUndoManager {
func registerUndo(closure: Void -> Void) {
let performer = SwiftUndoPerformer(closure: closure)
registerUndoWithTarget(performer, selector: Selector("performWithSelf:"), object: performer)
//(Passes unnecessary object to get undo manager to retain SwiftUndoPerformer)
}
}
Then you can Swift-ly register any closure:
undoManager.registerUndo {
self.myMethod()
}
setValue forKey does the trick for me on OS X if one needs to support 10.10. I couldn't set it directly cause prepareWithInvocationTarget returns a proxy object.
#objc
enum ImageScaling : Int, CustomStringConvertible {
case FitInSquare
case None
var description : String {
switch self {
case .FitInSquare: return "FitInSquare"
case .None: return "None"
}
}
}
private var _scaling : ImageScaling = .FitInSquare
dynamic var scaling : ImageScaling {
get {
return _scaling
}
set(newValue) {
guard (_scaling != newValue) else { return }
undoManager?.prepareWithInvocationTarget(self).setValue(_scaling.rawValue, forKey: "scaling")
undoManager?.setActionName("Change Scaling")
document?.beginChanges()
_scaling = newValue
document?.endChanges()
}
}
I too have done a bit of reading and came up with the following: I have 2 tableViews, source by a dictionary and array controller for playlists and its items respectively, which I'm adding to the Helium 3 project on GitHub (not there yet); here's a preview:
dynamic var playlists = Dictionary<String, Any>()
dynamic var playCache = Dictionary<String, Any>()
// MARK:- Undo keys to watch for undo: dictionary(list) and play item
var listIvars : [String] {
get {
return ["key", "value"]
}
}
var itemIvars : [String] {
get {
return ["name", "temp", "time", "rank", "rect", "label", "hover", "alpha", "trans"]
}
}
internal func observe(_ item: AnyObject, keyArray keys: [String], observing state: Bool) {
switch state {
case true:
for keyPath in keys {
item.addObserver(self, forKeyPath: keyPath, options: [.old,.new], context: nil)
}
break
case false:
for keyPath in keys {
item.removeObserver(self, forKeyPath: keyPath)
}
}
}
// Start or forget observing any changes
internal func observing(_ state: Bool) {
for dict in playlists {
let items: [PlayItem] = dict.value as! [PlayItem]
self.observe(dict as AnyObject, keyArray: listIvars, observing: state)
for item in items {
self.observe(item, keyArray: itemIvars, observing: state)
}
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let undo = self.undoManager {
let oldValue = change?[NSKeyValueChangeKey(rawValue: "old")]
let newValue = change?[NSKeyValueChangeKey(rawValue: "new")]
undo.registerUndo(withTarget: self, handler: {[oldVals = ["key": keyPath!, "old": oldValue as Any] as [String : Any]] (PlaylistViewController) -> () in
(object as AnyObject).setValue(oldVals["old"], forKey: oldVals["key"] as! String)
if !undo.isUndoing {
undo.setActionName(String.init(format: "Edit %#", keyPath!))
}
})
Swift.print(String.init(format: "%# %# -> %#", keyPath!, oldValue as! CVarArg, newValue as! CVarArg))
}
}
override func viewWillAppear() {
// Start observing any changes
observing(true)
}
override func viewDidDisappear() {
// Stop observing any changes
observing(false)
}
// "List" items are controller objects - NSDictionaryControllerKeyValuePair
internal func addList(_ item: NSDictionaryControllerKeyValuePair, atIndex index: Int) {
if let undo = self.undoManager {
undo.registerUndo(withTarget: self, handler: {[oldVals = ["item": item, "index": index] as [String : Any]] (PlaylistViewController) -> () in
self.removeList(oldVals["item"] as! NSDictionaryControllerKeyValuePair, atIndex: oldVals["index"] as! Int)
if !undo.isUndoing {
undo.setActionName("Add PlayList")
}
})
}
observe(item, keyArray: listIvars, observing: true)
playlistArrayController.insert(item, atArrangedObjectIndex: index)
DispatchQueue.main.async {
self.playlistTableView.scrollRowToVisible(index)
}
}
internal func removeList(_ item: NSDictionaryControllerKeyValuePair, atIndex index: Int) {
if let undo = self.undoManager {
undo.prepare(withInvocationTarget: self.addList(item, atIndex: index))
if !undo.isUndoing {
undo.setActionName("Remove PlayList")
}
}
if let undo = self.undoManager {
undo.registerUndo(withTarget: self, handler: {[oldVals = ["item": item, "index": index] as [String : Any]] (PlaylistViewController) -> () in
self.addList(oldVals["item"] as! NSDictionaryControllerKeyValuePair, atIndex: oldVals["index"] as! Int)
if !undo.isUndoing {
undo.setActionName("Remove PlayList")
}
})
}
observe(item, keyArray: listIvars, observing: false)
playlistArrayController.removeObject(item)
DispatchQueue.main.async {
self.playlistTableView.scrollRowToVisible(index)
}
}
"List" items are NSDictionaryControllerKeyValuePair for the NSDictionaryController.
The "item" handling is a bit more complicated but this should get you going. Each time a list or item is added/removed the proper the add|remove method is called. Then you start observing as new items appear and forget as they're removed, this also observes each object's ivars for changes.
Enjoy.
My current take on this:
protocol Undoable {
func inverted() -> Self
}
class Store<State, Action : Undoable> {
let undoManager : UndoManager
let state : State
let reducer : (inout State, Action) -> Void
//...init...
func send(_ action: Action) {
reducer(&state, action)
undoManager.registerUndo(withTarget: self){target in
target.send(action.inverted())
}
}
}
Works great if you're able to get the correct UndoManager. In SwiftUI, this seems to be tricky though, the one in the Environment does not seem to always be the one associated with command+z or Edit -> Undo (I even tried passing it as an argument to send from each View!), and even making it a computed property like below didn't solve my problem:
var undoManager : UndoManager? {
NSApplication.shared.keyWindow.undoManager
}
Edit: my bad, passing it as a function argument works just fine. Just not from sheets apparently, because they're in their own NSWindow... One has to pass the proper UndoManager down then. If the sheet has a deeply nested view hierarchy, one should pass it through a custom EnvironmentValue.

Resources