I am working on my first Apple Watch app (an extension to my iOS app). I am facing a small problem in sending data from one WKInterfaceController to another.
My First Controller (InterfaceController.swift) has didReceiveMessage where it receives data from my iOS app.
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let myQrValue = message["qrCode"] as! String
let myQrImage = message["qrCodeImageData"] as! Data
var myData: [AnyHashable: Any] = ["myQrValue": myQrValue, "myQrImage": myQrImage]
if myQrValue.isEmpty == false {
WKInterfaceController.reloadRootControllers(withNames: ["QrScreen"], contexts: [myData])
}
}
Then in my Second Controller (QrInterfaceController.swift), I am having below to fetch the data sent from the first controller -
override func awake(withContext context: Any?) {
super.awake(withContext: context)
print("context \(context)")
if let myData = context {
print("myData \(myData)")
// userQrNumber.setText(myData)
}
if let myQrImage = myQrImage {
userQrImage.setImageData(myQrImage)
}
if let myQrLabel = myQrLabel {
userQrNumber.setText(myQrLabel)
}
self.setTitle("")
}
I am stuck (could be simple/silly question) as how to parse my data from the context in the second controller?
Also, the didReceiveMessage works only the second time when I launch my ViewController where the sendMessage code is placed. Is it normal?
First, you might want to redeclare myData as this:
var myData: [String: Any] = ...
which makes it a bit simpler. Then, in the awake function, you’d go ahead like this:
if let myData = context as? [String: Any] {
if let myQrImage = myData["myQrValue"] as? Date {
...
Does this show you the right direction?
I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.
I'm searching for a clean way to retrieve (and sometimes save) data from Firebase in Swift. It's annoying me that all my database calls are written in the middle of the view controller code. So I'm looking for some kind of custom data service class. I found this tutorial that's close to what I want: http://www.mobilecyberpunks.com/?p=82.
They promised a Part II but I cannot find this second part, so I guess this was never made. In this second part they promised to cover retrieving and saving data with this custom data service (which is the most important part of the whole thing for me).
I'm thinking of an API class (like in the tutorial) and when I'm retrieving data, and it finishes retrieving from firebase, I save it in a data set in this api class. Then I will posting a notification with Notification Center. But I'm am not sure whether this is best practice or a good way to do this.
Has anyone an idea how to do this (finishing this tutorial I found or in another way)?
Thanks in advance!
Making a custom Class for the communicating is generally a good idea if you need extensive function's and make numerous calls to your server.
The two preferred methods for this are:-
Protocol-Delegate Method
_completionBlocks:
Below answer contains both.
Custom Class
import Foundation
import Firebase
#objc protocol FIRShowAlertDelegate {
func showFIRAlert(_ message : String)
#objc optional func activityIndic()
}
class FIRController :{
var delegate : FIRShowAlertDelegate!
func loginUser(_ emailAddress : String!, password : String , completionBlock : #escaping ((currentUserID : String!) -> Void)){
FIRAuth.auth()?.signIn(withEmail: emailAddress, password: password,
completion: {(user,err) in
if err != nil{
self.delegate.showFIRAlert("Error logging you in,\(err?.localizedDescription)")
}else{
completionBlock(user!.uid)
}
})
}
func retrieveUserData(_ currentId : String!, completionBlock : #escaping ((_ userName : String?) -> Void)){
FIRDatabase.database().reference().child("Users").child(currentId).observeSingleEvent(of: .value, with: {(userSnap) in
if userSnap.exists(){
if let userDict = userSnap.value! as? [String:AnyObject]{
completionBlock(userDict["username"] as! String
}
}else{
completionBlock(nil, nil)
print("No such user exists: \(currentId)")
}
})
}
}
Your ViewController
class AnyViewController : UIViewController, FIRShowAlertDelegate{
let firebaseControllerHandle : FIRController = FIRController()
override func viewDidLoad() {
super.viewDidLoad()
firebaseControllerHandle.delegate = self
firebaseControllerHandle.loginUser("abc#xyz.com", password: "123454321", completionBlock: { (userID) in
print("user : \(userID), logged in")
})
}
func showFIRAlert(_ message : String){
let alertController : UIAlertController = UIAlertController(title: "MyApp", message: message, preferredStyle: .alert)
let okAction : UIAlertAction = UIAlertAction(title: "Ok", style: .default) { (alert) in
print("User pressed ok function")
}
alertController.addAction(okAction)
alertController.popoverPresentationController?.sourceView = view
alertController.popoverPresentationController?.sourceRect = view.frame
self.present(alertController, animated: true, completion: nil)
}
func activityIndic() {
// Use for showing the activity indicator while the data is being retrieved
}
}
I started to use this solution and polished it a little bit, and I came to a pretty handy solution.
I created a custom class named FirebaseAPI. This is a singleton class. This class contains all the methods for Firebase (Authentication, Database, Storage, ...).
Example:
FirebaseAPI.swift
import FirebaseAuth
import FirebaseDatabase
class FirebaseAPI {
static let shared = FirebaseAPI()
private init() {}
//Authentication
func logInUser(onCompletion: #escaping (String?) -> Void {
FIRAuth.auth().signInAnonymously(completion: {(user, error) in
if error == nil {
onCompletion(user!.uid)
} else {
onCompletion(nil)
}
})
}
//Database
func getObjects(parameter: ParamaterClass, onCompletion: #escaping ([ObjectClass]) -> Void) {
Constants.Firebase.References.Object?.observe(.value, with: { snapshot in
var objects = [ObjectClass]()
if snapshot.exists() {
for child in snapshot.children.allObjects {
let object = Object(snapshot: child as! FIRDataSnapshot)
objects.append(object)
}
}
onCompletion(objects)
})
}
}
Constants.swift
import FirebaseDatabase
struct Constants {
struct Firebase {
static var CurrentUser: FIRDatabaseReference?
static var Objects: FIRDatabaseReference?
}
}
AppDelegate.swift
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FIRApp.configure()
FirebaseAPI.shared.logInUser(onCompletion { uid in
if uid != nil {
Constants.Firebase.References.CurrentUser = FIRDatabase.database().reference().child("users").child(uid!)
Constants.Firebase.References.CurrentUser.keepSynced(true)
Constants.Firebase.References.Objects = FIRDatabase.database().reference().child("objects")
Constants.Firebase.Reference.Objects?.keepSynced(true)
}
})
}
return true
}
I can give you a example of calling methods in the FirebaseAPI in a ViewController, but an example of such a method is given in the code of the AppDelegate.swift up here (the FirebaseAPI.shared.logInUser method).
Used this structure in 3 different projects up till now and it works fluently!
I'm doing an Apple Watch App, with a Complication.
I've got the WatchKit App part working great with this Ev class...
class Ev {
var evTColor:String
var evMatch:String
init(dataDictionary:Dictionary<String,String>) {
evTColor = dataDictionary["TColor"]!
evMatch = dataDictionary["Match"]!
}
class func newEv(dataDictionary:Dictionary<String,String>) -> Ev {
return Ev(dataDictionary: dataDictionary)
}
}
... and this InterfaceController
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TColor"] as? String, let matchValue = userInfo["Match"] as? String {
receivedData.append(["TColor" : tColorValue , "Match" : matchValue])
evs.append(Ev(dataDictionary: ["TColor" : tColorValue , "Match" : matchValue]))
doTable()
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func doTable() {
self.rowTable.setNumberOfRows(self.evs.count, withRowType: "rows")
for (index, evt) in evs.enumerate() {
if let row = rowTable.rowControllerAtIndex(index) as? TableRowController {
row.mLabel.setText(evt.evMatch)
row.cGroup.setBackgroundColor(colorWithHexString(evt.evTColor))
} else {
print("nope")
}
}
}
I'm having a hard time getting the same sort of thing to work in my Complication, any ideas?
I'm not sure if I can just use the same Ev code for my ExtensionDelegate, and then what exactly to put in my ComplicationController.
If I use the same Ev code in my ExtensionDelegate I'm getting a fatal error: use of unimplemented initializer init().
And in my ComplicationController I'm not sure how to go about best using the data I already have from InterfaceController to fill out the getCurrentTimelineEntryForComplication &getTimelineEntriesForComplication methods in ComplicationController.
Will post any extra code as needed, thanks!
EDIT:
Per a question, my data comes from CloudKit to the iPhone App (which I then pass to the Watch App via WCSession, so my problem is accessing that data in my Watch App for my Complication)
Instead of having your InterfaceController implement and receive the WCSession messages, I would set up a singleton class that receives those messages instead. That class can parse and organize your user info data from the WCSession. That singleton class can/will be accessible in your ComplicationController and your InterfaceController
Singletons are fairly easy to setup in swift:
class DataManager : WCSessionDelegate {
// This is how you create a singleton
static let sharedInstance = DataManager()
override init() {
super.init()
if WCSession.isSupported() {
self.watchConnectivitySession?.delegate = self
self.watchConnectivitySession?.activateSession()
}
}
// This is where you would store your `Ev`s once fetched
var dataObjects = [Ev]()
// This is the method that would fetch them for you
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
//parse your userInfoDictionary
self.dataObjects = evs
}
}
Then in your InterfaceController you can reference it using DataManager.sharedInstance.dataObjects to build your InterfaceController or ComplicationsController
The idea with a singleton is that you have a one global reference. DataManager only gets instantiated once and only once.
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.