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.
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
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.
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)
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.