Set the same callback closure at two different places in code - ios

Im wondering how from the side of performance and reference counting setting the same callback at two different places in code would affect the app? Can memory leak happen or something similar?
Lets take a look at this example:
viewController has a tableView which is set from tableViewModel
tableViewModel is connected with cell through cellViewModel
in cellViewModel we define callback:
var titleCallback: ((String) -> Void)?
var title: String {
didSet{
titleCallback?(title)
}
}
in cell we set titleCallback:
func configCell(_ cellViewModel: CellViewModel) {
cellViewModel.titleCallback = { [weak self] title in
titleLabel.text = title
}
}
My question: Is it ok to set the same callback in tableViewModel so we can pass title up to viewController?
in tableViewModel we set it like this:
var sendTitleToViewControllerCallback: ((String) -> Void)?
cellViewModel.titleCallback = { [weak self] title in
sendTitleToViewControllerCallback?(title)
}
The same callback is set and triggered at two different places in code. What is the expected behavior for this and are there any drawbacks with this approach?

Related

SwiftUI View Extension is called on View load/refresh

We created a custom view extension to extend the functionality of .alert and add a bunch of customizations to fit with our needs;
import SwiftUI
extension View {
public func alertX(isPresented: Binding<Bool>, content: () -> AlertX) -> some View {
let alertX_view = AlertX_View(visible: isPresented, alertX: content())
let alertXVC = AlertXViewController(alertX_view: alertX_view, isPresented: isPresented)
alertXVC.modalPresentationStyle = .overCurrentContext
alertXVC.view.backgroundColor = UIColor.clear
alertXVC.modalTransitionStyle = .crossDissolve
if isPresented.wrappedValue == true {
if AlertX_View.currentAlertXVCReference == nil {
AlertX_View.currentAlertXVCReference = alertXVC
}
let viewController = self.topViewController()
viewController?.present(alertXVC, animated: true, completion: nil)
} else {
alertXVC.dismiss(animated: true, completion: nil)
}
return self
}
... truncated for brevity
}
It is called on a view in the same way as .alert is called;
.alertX(isPresented: $viewModel.showAlert) {
AlertX(logo: networkStore.currentNetwork.networkTheme.systemLogo ,
logoColor: networkStore.activeColors.lightTextColor ,
backgroundColor: networkStore.activeColors.primaryColor,
title: Text("AlertX Test"))
}
Where $viewModel.showAlert is #Published var showAlert: Bool and AlertX is a custom view that contains our proprietary customizations to fit with our B2B application.
The issue I am having is that the information inside the closure for .alertX(isPresented: is called every time the view loads or changes state, regardless of whether or not the $viewModel.showAlert binding changes. The same is NOT true of the built in .alert view extension which is ONLY called when the $viewModel.showAlert value changes.
What modifications do I need to make in my implementation of public func alertX(isPresented: Binding<Bool>, content: () -> AlertX) -> some View { so that the information inside the closure it is only called when the binding value changes?
Remember that SwiftUI will reload all of the structs and functions all the time - and then decides, based on the data dependencies, which part to redraw. So any code in your view functions will run. A lot.
Try this: at the top of that function, put
guard isPresented.wrappedValue == true else {
return
}
Instead of checking it later on in the function.
Related, you might also want to look into making this code into a ViewModifier, and then your alertX function just applies the viewModifier based on the isPresented binding, otherwise returns self.

iOS/Swift - What is the difference between Closure/Completion blocks and Delegates/functions?

I don't clear about these two, Nowadays the world is shifting to the closure types. But I'm not clearly understanding this. Can someone explain me with a real-time example?
So a real life example of both would be something like this:
protocol TestDelegateClassDelegate: class {
func iAmDone()
}
class TestDelegateClass {
weak var delegate: TestDelegateClassDelegate?
func doStuff() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.delegate?.iAmDone()
}
}
}
class TestClosureClass {
var completion: (() -> Void)?
func doStuff() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.completion?()
}
}
}
class ViewController: UIViewController, TestDelegateClassDelegate {
func iAmDone() {
print("TestDelegateClassDelegate is done")
}
override func viewDidLoad() {
super.viewDidLoad()
let testingDelegate = TestDelegateClass()
testingDelegate.delegate = self
testingDelegate.doStuff()
let testingClosure = TestClosureClass()
testingClosure.completion = {
print("TestClosureClass is done")
}
testingClosure.doStuff()
}
}
Here we have 2 classes TestDelegateClass and TestClosureClass. Each of them have a method doStuff which waits for 3 seconds and then reports back to whoever is listening where one uses delegate procedure and the other one uses closure procedure.
Although they do nothing but wait you can easily imagine that they for instance upload an image to server and notify when they are done. So for instance you might want to have an activity indicator running while uploading is in progress and stop it when done. It would look like so:
class ViewController: UIViewController, TestDelegateClassDelegate {
#IBOutlet private var activityIndicator: UIActivityIndicatorView?
func iAmDone() {
print("TestDelegateClassDelegate is done")
activityIndicator?.stopAnimating()
}
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator?.startAnimating()
let testingDelegate = TestDelegateClass()
testingDelegate.delegate = self
testingDelegate.doStuff()
activityIndicator?.startAnimating()
let testingClosure = TestClosureClass()
testingClosure.completion = {
self.activityIndicator?.stopAnimating()
print("TestClosureClass is done")
}
testingClosure.doStuff()
}
}
Naturally you would only use one of the two procedures.
You can see there is a huge difference in code. To do a delegate procedure you need to create a protocol, in this case TestDelegateClassDelegate. A protocol is what defines the interface of a listener. And since an iAmDone method is defined it must be defined in ViewController as well as long as it is defined as TestDelegateClassDelegate. Otherwise it will not compile. So anything declared as TestDelegateClassDelegate will have that method and any class can call it. In our case we have weak var delegate: TestDelegateClassDelegate?. That is why we can call delegate?.iAmDone() without caring what delegate actually is. For instance we can create another class:
class SomeClass: TestDelegateClassDelegate {
func iAmDone() {
print("Something cool happened")
}
init() {
let testingDelegate = TestDelegateClass()
testingDelegate.delegate = self
testingDelegate.doStuff()
}
}
So a good example for instance is an UITableView that uses a delegate and dataSource (both are delegates, just properties are named differently). And table view will call the methods of whatever class you set to those properties without needing to know what that class is as long as it corresponds to the given protocols.
Same could be achieved with closures. A table view could have been defined using properties giving closures like:
tableView.onNumberOfRows { section in
return 4
}
But that would most likely lead into one big mess of a code. Also closures would in this case be giving many programmers headaches due to potential memory leaks. It is not that closures are less safe or anything, they just do a lot of code you can't see which may produce retain cycles. In this specific case a most likely leak would be:
tableView.onNumberOfRows { section in
return self.dataModel.count
}
and a fix to it is simply doing
tableView.onNumberOfRows { [weak self] section in
return self?.dataModel.count ?? 0
}
which now looks overcomplicated.
I will not go into depths of closures but in the end when you have repeated call to callbacks (like in case of table view) you will need a weak link either at delegate or in closure. But when the closure is called only once (like uploading an image) there is no need for a weak link in closures (in most but not all cases).
In retrospective use closures as much as possible but avoid or use caution as soon as a closure is used as a property (which is ironically the example I gave). But you would rather do just this:
func doSomethingWithClosure(_ completion: #escaping (() -> Void)) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
completion()
}
}
And use it as
doSomethingWithClosure {
self.activityIndicator?.stopAnimating()
print("TestClosureClass is done")
}
This has now removed all potential risks. I hope this clears a thing or two for you.
In Swift/obj-c the term delegate is used to refer to a protocol that responds to specific selectors.
Thing about it just like calling a method on an object.
E.g.
protocol CalculatorDelegate : class { // : class so it can be made 'weak'
func onCalculation(result: Int) -> Void
}
Now if we have a Calculator class, to use the delegate we'd do something like
class Calculator() {
weak var delegate: CalculatorDelegate?
func calculate(_ a: Int, _ b: Int) -> Int {
let result = a + b
self.delegate?.onCalculation(result: result)
return result
}
}
Then in some other class (e.g. in iOS - a View Controller) we might do:
class MyClass : CalculatorDelegate {
func onCalculation(result: Int) {
print("Delegate method on calculation called with result \(result)")
}
func someButtonPress() {
let calculator = Calculator()
calculator.delegate = self
calculator.calculate(42, 66)
}
}
So you can see how the setup is quite elaborate.
Now closures are simply blocks of code that can be called in other places, so you could change all of the code like so:
class Calculator2() {
weak var delegate: CalculatorDelegate?
func calculate(_ a: Int, _ b: Int, onCalculation: (#escaping (Int) -> Void) -> Int)?) {
let result = a + b
onCalculation?(result)
return result
}
}
class MyClass {
func someButtonPress() {
let calculator = Calculator2()
calculator.calculate(42, 66, onCalculation: { (result: Int) in
print("Closure invoked with \(result)")
})
}
}
However, with closures you need to be aware that it is a lot easier to shoot yourself in the foot by capturing variables (e.g. self) strongly, which will lead to memory leaks even under ARC regime.
Closures are first-class objects so that they can be nested and passed around
Simply,
In swift, functions are primitive data types like int, double or character that is why you can pass a function in the function parameter. In swift mechanism simplify in closures syntax like lambda expression in other languages.
E.g. If you want to call rest API through URSSession or Alamofire and return response data then you should use completionHandler(it's closure).
Void closure : - {(paramter:DataType)->Void}
Return closure : - {(paramter:DataType)->DataType}
e.g. (int, int) -> (int)
https://docs.swift.org/swift-book/LanguageGuide/Closures.html

Counting beans with ReactiveCocoa 4 and an NSButton

I have the following:
Two interesting classes: a ViewController and a ViewModel
A button nsButtonMorePlease:NSButton in the view of ViewController
A text box nsTextView:NSTextView in the view as well
I want the following behavior:
When you launch the program, the "count" starts at 0 and is displayed in the text box nsTextView
When you press the button nsButtonMorePlease, the count is incremented by 1 and the updated count is reflected in the nsTextView
I would like to ensure:
I use ReactiveCocoa 4 (that's the point kind of)
The model class contains numberOfBeans: MutableProperty<Int> starting at 0
The design is purely functional or close to it - that is (if I understand the term), every link in the chain mapping the event of mouse click to the MutableProperty of numberOfBeans to responding to it in the text view, is all without side effects.
Here's what I have. Fair warning: this doesn't come close to working or compiling I believe. But I do feel like maybe I want to use one of combineLatest, collect, reduce, etc. Just lost on what to do specifically. I do feel like this makes something easy quite hard.
class CandyViewModel {
private let racPropertyBeansCount: MutableProperty<Int> = MutableProperty<Int>(0)
lazy var racActionIncrementBeansCount: Action<AnyObject?, Int, NoError> = {
return Action { _ in SignalProducer<Int, NoError>(value: 1)
}
}()
var racCocoaIncrementBeansAction: CocoaAction
init() {
racCocoaIncrementBeansAction = CocoaAction.init(racActionIncrementBeansCount, input: "")
// ???
var sig = racPropertyBeansCount.producer.combineLatestWith(racActionIncrementBeansCount.)
}
}
class CandyView: NSViewController {
#IBOutlet private var guiButtonMoreCandy: NSButton!
#IBOutlet private var guiTextViewCandyCt: NSTextView!
}
class CandyViewModel {
let racPropertyBeansCount = MutableProperty<Int>(0)
let racActionIncrementBeansCount = Action<(), Int, NoError>{ _ in SignalProducer(value: 1) }
init() {
// reduce the value from the action to the mutableproperty
racPropertyBeansCount <~ racActionIncrementBeansCount.values.reduce(racPropertyBeansCount.value) { $0 + $1 }
}
}
class CandyView: NSViewController {
// define outlets
let viewModel = CandyViewModel()
func bindObservers() {
// bind the Action to the button
guiButtonMoreCandy.addTarget(viewModel.racActionIncrementBeansCount.unsafeCocoaAction, action: CocoaAction.selector, forControlEvents: .TouchUpInside)
// observe the producer of the mutableproperty
viewModel.racPropertyBeansCount.producer.startWithNext {
self.guiTextViewCandyCt.text = "\($0)"
}
}
}

How to downcast / cast a struct's generic type in Swift

I've refactored my work with UICollectionViewCells into the following
struct CollectionViewCellModel<T: UICollectionViewCell> {
let reuseIdentifier: NSString
let allowsSelection: Bool
// Optional callbacks
var onCreate: ((T) -> Void)? = nil
var onSelection: ((T) -> Void)? = nil
var onWillBeDisplayed: ((T) -> Void)? = nil
var onDelete: ((T) -> Void)? = nil
// Create cell
func toCell(collectionView: UICollectionView, indexPath: NSIndexPath) -> T {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as T
if let onCreate = onCreate { onCreate(cell) }
return cell
}
}
This has made it easier for me to create a list of specific cells (For things like forms) and then work with them.
However, I keep getting tripped up in how to store these objects. I can't downcast them to CollectionViewCellModel<UICollectionViewCell> and therefore can't store a list of [CollectionViewCellModel<UICollectionViewCell>
I thought Swift supported down-casting, but apparently not for generic types?
let cell = CollectionViewCellModel<A>(...)
let genericCell = cell as CollectionViewCellModel<UICollectionViewCell>
// ERROR: UICollectionViewCell is not identical to A
let genericMaybeCell = cell as? CollectionViewCellModel<UICollectionViewCell>
// ERROR: UICollectionViewCell is not identical to A
Do I have to store these as an array of Any and then cast them every time or am I just misunderstanding something (or both)?
Update: I've done some work in the playground to clearly illustrate what I mean:
protocol IA {
func name() -> String
}
class A:IA { func name() -> String { return "A (Base)" } }
class B: A { override func name() -> String { return "B (Child of A)" } }
class C: B { override func name() -> String { return "C (Child of B)" } }
struct SA<T: A> {
}
let struct0: Any = SA<B>()
// OK: yes, the struct of B is an Any!
// but since B inherits from A, isn't it safe to
// say that SA<B> is also of type SA<A>?
let struct1: SA<A> = SA<B>() as SA<A>
// NO
// ERROR: 'B' is not identical to 'A'
let struct1Optional: SA<A> = SA<B>() as? SA<A>
// not even optionally? NO
// ERROR: 'B' is not identical to 'A'
I guess it's not possible. Maybe in Swift 1.3. See thread in comments.
Update (2/17/15)
For those of you interested in why I'm even doing this in the first place, you have to understand how I'm working with my CollectionViewControllers (CVCs). I've abstracted a base CVC to perform the common methods every screen needs. This CVC has a protocol that expects a Factory that creates CVC models. These models know how to transform themselves, respond to actions, and very much act like a controller. They're fat & active. My views on the other hand are all dumb. All they know how to do is move stuff around on the screen or go into different display states. When configuring a cell from a CVC, you end up doing these big switch statements that really don't tell you much from a readability perspective expect "route this". It gets worse you start to configure your view in there. It's not horrible, but generally speaking -- to me -- a view controller controls the view it's responsible for. While this VC might be the parent VC of the cell, this does not give it proper access to manipulate it heavily. This now makes your VC do things like cell.changeDisplayToActiveState(); in short, it now bears the burden of controlling its children cells. This is what contributes to all the "fat" VCs out there. I first chose to deviate from this path by adopting the VIPER pattern, but I found it overkill -- especially for a new project. I scrapped it and began to work with a base CVC. This is currently how my project works:
A base CVC expects a factory & factory expects a CVC (thus facilitating two way binding for the cells)
This factory produces cell & header models for THE CVC.
These models have callback & data & configuration functionality embedded within them.
They can call PUBLIC functions of the view controller from these callbacks (thus strictly separating responsibilities) such as vc.changeDisplayState(...) or changeToVc(...)
I'm not sure exactly what you're after, but I'll try to answer. Presumably you are not interested in storing a bunch of CollectionViewCellModel<X>'s for some specific X, because then the answer to "how do I store them?" would be simple: [CollectionViewCellModel<X>]. So I'm guessing you want a you want an array of CollectionViewCellModel<T> for heterogeneous T's. The best way to achieve that is to give all your CollectionViewCellModel<T>'s a common protocol (or base class, but in your case they're structs, so not that), something roughly like this:
protocol CollectionViewCellModelType {
var reuseIdentifier: NSString {get}
var allowsSelection: Bool {get}
func toCell(
collectionView: UICollectionView, indexPath: NSIndexPath
) -> UICollectionViewCell
}
struct CollectionViewCellModel<T: UICollectionViewCell>
: CollectionViewCellModelType {
let reuseIdentifier: NSString
let allowsSelection: Bool
// Optional callbacks
var onCreate: ((T) -> Void)? = nil
var onSelection: ((T) -> Void)? = nil
var onWillBeDisplayed: ((T) -> Void)? = nil
var onDelete: ((T) -> Void)? = nil
// Create cell
func toCell(
collectionView: UICollectionView, indexPath: NSIndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
reuseIdentifier, forIndexPath: indexPath) as! T
if let onCreate = onCreate { onCreate(cell) }
return cell
}
}
var a: [CollectionViewCellModelType] = [] // put them in here
Hoping this helps,
Dave

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