Xcode 10.1, Swift 4.2, Realm 3.12 (database)
I have a Mac app where multiple users collaborate and the data syncs across multiple devices. So data is always changing.
Throughout the app, I write code that updates the UI when a sync occurs in the background. A problem I keep running into is that when a user is typing in an NSTextField, a sync will happen (from another user's changes) and the NSTextField where they are typing will suddenly revert to the new sync value, and the user will lose what they typed.
Here is a demo Mac app I put together that simulates the problem. If you start typing in the field, every 10 seconds the field updates: https://d.pr/f/8iXjqx
I'm wondering what strategies other Mac devs have used to avoid these collisions.
Is there a way to tell an NSTextField to not update if there is a cursor present in it? Do I have to save the value to my local database with every keystroke and then somehow do a merge when a sync happens?
In your textFields I would catch whether or not you are active:
override func becomeFirstResponder() -> Bool
{
isFirstResponder = true
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool
{
isFirstResponder = false
return super.resignFirstResponder()
}
Then when you go to update the text, leave it if that flag is set.
Assuming you don't want to merge the text in some way, refresh the text to the model value if no edits have been made when you resign, otherwise write the change back to the model.
Related
I have a basic NSDocument-based app. I need to make one last change to the document when:
The user closes the document's window
The user terminates the app
Why?
The document window contains an NSTextField. Usually text entered into this text field is committed to the document's model after the user presses Enter (via textDidEndEditing(_:)).
Let's assume the user typed some text, but does not press Enter. Instead he presses Cmd-W or Cmd-Q to close the document window or terminate the app altogether.
textDidEndEditing is not called so I check if the text field contains changes and try to update the document myself.
❌ Here is where it gets tricky. The following results in a deadlock on NSDocument.performActivityWithSynchonousWaiting:
override func viewWillDisappear() {
super.viewWillDisappear()
undoManager?.disableUndoRegistration()
textField.commitEditing() // Updates the model
document.updateChangeCount(.changeDone)
}
I managed to work around the deadlock by not hooking into viewWillDisappear, but into NSDocument.canClose(withDelegate delegate: Any, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?).
✅ This code causes the changes to be saved when the user closes the document window:
override func canClose(withDelegate delegate: Any, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) {
undoManager?.disableUndoRegistration()
textField.commitEditing() // Updates the model
updateChangeCount(.changeDone)
super.canClose(withDelegate: delegate, shouldClose: shouldCloseSelector, contextInfo: contextInfo)
}
.
❓Unfortunately, the above is not called when the user terminates the app. I tried updating my document in applicationWillTerminate() -- did not work. I also tried overriding applicationShouldTerminate() and delaying termination for 3 seconds. I can see the document's window being marked as "edited", but changes are not saved.
How can I make a last change to NSDocument right before the app terminates and have it saved automatically?
For reference: I don't think it is possible to make a last change to NSDocument while the app is being terminated. The document architecture is simply not designed that way (see https://www.mail-archive.com/cocoa-dev#lists.apple.com/msg107739.html)
At the time the app is terminating, the document's windows have been closed and window controllers released.
I eventually solved this problem by changing the design of my app to record changes to the document as the user types. I use a new instance of UndoManager and manually call NSDocument.updateChangeCount(.changeDone) to register my changes out of the undo/redo chain. When the user finally commits changes by pressing Enter, I use the document's undo manager to register the change.
I'm a newcomer to Swift (2.2) and am having a problem with a simple app using Xcode 7.3 and OS X 10.11. In this app, the user clicks a button and selects a file through NSOpenPanel. The code uses the URL selected to get the file's data and name, then processes the data and saves the result somewhere else. With large files, the processing can take several seconds. When processing large files, once the file is selected, the space where the Open File window had been remains blank, covering the app view and everything else, and stays there until the operation is complete. Also, the app's outlets are frozen until the operation finishes. It appears NSOpenPanel isn't handing window control back to the app and the system.
The code goes like this:
#IBAction func processFile(sender: AnyObject) {
var chosenURL: NSURL?
let openPanel = NSOpenPanel()
openPanel.title = "Choose a file"
openPanel.canChooseDirectories = false
openPanel.allowsMultipleSelection = false
if openPanel.runModal() == NSFileHandlingPanelOKButton {
chosenURL = openPanel.URL
}
let dataBytes = NSData(contentsOfURL: chosenURL!)
let fileName = chosenURL!.lastPathCompnent!
// Remaining code processes dataBytes and fileName
I've tried a few variations but get the same result. Searching for "NSOpenPanel won't close" on the 'net usually just brings up examples in Objective-C, which I know nothing of. Any suggestions of how to make NSOpenPanel shut off and have view and control return to the app window?
Following on from Eric D's suggestion, I looked into Grand Central Dispatch and background processes. My first approach was:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
dataBytes = NSData(contentsOfURL: chosenURL!)
}
That didn't change anything. I found I had to put the whole remaining process (everything from 'let dataBytes…' onwards) within the dispatch closure, with 'dispatch_async(dispatch_get_main_queue())' statements around UI updates. This stopped the window from freezing and blanking, and returned control to the app. Thanks again, Eric.
I have a status bar application and when I'm opening it, it shows me informations but if these informations are changing (percentage here), I don't see it directly. I must reopen it to make it shows new informations.
Here I open the app one time it's 90% :
Then I wait some time and reopen it, it's already 100% :
Is there a way to show "in real time" labels and stuff in a status bar application ?
Having come across it before:
https://github.com/adamhartford/PopStatusItem
Does quite well, appears to be thread friendly (having tested with async threads) on the popup item..
Edit
addition as the user needs the actual status bar Image/text updated as opposed to the popup...
let sysStatusBar = NSStatusBar.systemStatusBar()
dispatch_async(dispatch_main(){
let dele = NSApp.delegate as? AppDelegate
dele.statusBarValue = 100.0
// from the delegate or a singleton method, have statusbarValue observed
// and update the App.delegate.statusBarItem.image accordingly.
// make sure it happens on the main thread... then the image / text
// will update...
}
What you're doing is updating the delegate where you've added the NSStatusBarItem and updating the image there. IN my example, I update the "statusBarValue" but if you just add a binding or an observer to the value, you can just as easily update the image.
ONCE AGAIN Make sure this happens on the main thread, or the UI is just going to ignore your updates. So updates from background threads etc... need to call out on the main thread.
I'm trying to use CGAssociateMouseAndMouseCursorPosition(NO) in a program. This disconnects the mouse from the on screen cursor when your application is "in the foreground". Unfortunately it also disconnects it when Mission Control or the application switcher or who knows what else comes up.
So far I know:
The application is still active.
The window is still key.
Nothing is sent to the default notification center when these things come up.
The application stops receiving mouse moved events, but an NSEvent addGlobalMonitorForEventsMatchingMask:handler: also does not receive them, which is strange to say the least. It should receive any events not delivered to my application. (I was planning to detect the missing events to know when to associate the mouse again.
So, is there a way to detect when my application is no longer in control, specifically because Mission Control or the switch has taken over? They really expect the mouse to work and I need to restore that association for them.
I share your surprise that a global event monitor isn't seeing the events. In a similar situation, I used a Quartz Event Tap for a similar purpose. The Cocoa global event monitor is quite similar to event taps, so I figured it would work.
I put the tap on kCGAnnotatedSessionEventTap and compared the result from CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) to getpid() to determine when the events were going to another app (e.g. Mission Control or Exposé). (I disable the tab when my app resigns active status, so it should only receive events destined for another app when this sort of overlay UI is presented.)
By the way, you mentioned monitoring the default notification center, but, if there's a notification about Mission Control or the like, it's more likely to come to the distributed notification center (NSDistributedNotificationCenter). So, it's worth checking that.
I needed to check for mission control being active and ended up with an approach along the lines of Ken's answer.
Sharing is caring so here is the smallest sensible complete code that worked for me: (Swift 5)
import Foundation
import AppKit
let dockPid = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.processIdentifier
var eventTargetPid: Int32?
let eventTap = CGEvent.tapCreate(
tap: .cgAnnotatedSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly,
eventsOfInterest: CGEventMask(
(1 << CGEventType.mouseMoved.rawValue)
| (1 << CGEventType.keyDown.rawValue)
),
callback: { (tapProxy, type, event, _:UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? in
// Now, each time the mouse moves this var will receive the event's target pid
eventTargetPid = Int32(event.getIntegerValueField(.eventTargetUnixProcessID))
return nil
},
userInfo: nil
)!
// Add the event tap to our runloop
CFRunLoopAddSource(
CFRunLoopGetCurrent(),
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0),
.commonModes
)
let periodSeconds = 1.0
// Add a timer for periodic checking
CFRunLoopAddTimer(CFRunLoopGetCurrent(), CFRunLoopTimerCreateWithHandler(
kCFAllocatorDefault,
CFAbsoluteTimeGetCurrent() + periodSeconds, periodSeconds, 0, 0,
{ timer in
guard eventTargetPid != dockPid else {
print("Dock")
return
}
print("Not dock")
// Do things. This code will not run if the dock is getting events, which seems to always be the case if mission control or command switcher are active
}), .commonModes)
CFRunLoopRun()
This simply checks whether the dock was the one to receive the last event of interest (here that includes mouse movement and key-downs).
It covers most cases, but will report the wrong value between the command switcher or mission-control hiding and the first event being sent to a non-dock app. This is fine in my use-case but could be an issue for other ones.
Also, of course, when the dock at the bottom is active, this will detect that too.
Have you tried asking NSRunningApplication?
I have a simple form (NSWindow) with 3 text fields. NSWindow's initialFirstResponder is 'pointing' to the first field (NSTextField). All three text fields are circularly linked to each other via nextKeyView.
Problem that I have is that when I start the application from Xcode it'll focus on the text field that was last active (in focus) when the application closed.
So for example, if I name text fields A, B and C and initialFirstResponder is set to A. Now if I start the application, focus on B, and close the application, next time I start it, the focus will be on B.
Why is that and how would I fix this?
(Sorry if this is a trivial question, these are my first steps in cocoa...)
EDIT:
This is on OS X Lion 10.7.1, Xcode 4.1.
EDIT 2:
I found a way to "fix" this... In the main window (or any window for that matter) XIB/NIB file, click on "Attributes Inspector", then uncheck "Restorable" box. Now the application will not store the last position and so the initialFirstResponder seeing will be respected and followed accordingly.
Welcome to Cocoa! :) I suspect this is happening as part of the new user interface preservation features in OS X Lion. (In fact, I just created a simple app with 3 text fields, and I see this behavior too.) Because windows automatically restore themselves, you will see a lot of this behavior happening automatically even if you didn't implement it. This is probably desirable — most applications will work this way, and the user will come to expect it.
However, if you really want to disable it, you can probably do so by subclassing NSWindow or perhaps NSTextField and overriding -encodeRestorableStateWithCoder:. But, I definitely recommend you leave the default behavior alone.
Edit with a little further information: the app state seems to be stored in ~/Library/Saved Application State/com.yourapp.savedState. There you can see a plist file with information about the windows. The other files don't seem easily readable, but they probably contain information about which field is first responder, etc.
Despite this thread being almost 10 years old I'll gonna add an answer. Just about one month after the answer from jbandes OS X 10.7 Lion was introduced.
Following a quote from NSWindowRestoration.h
#interface NSWindow (NSUserInterfaceRestoration)
/* Determines whether the window should be restored on relaunch. By default, windows with NSTitledWindowMask set in the styleMask are restorable, and windows without it set are not.
*/
#property (getter=isRestorable) BOOL restorable API_AVAILABLE(macos(10.7));