Post

Replies

Boosts

Views

Activity

Reply to UIDocumentViewController missing page background in browser on iPadOS 27
Many thanks for the reply. That's great to know the engineering team have the UIDocumentViewController bug in hand. I'll definitely be testing each new version as I'm full steam ahead on macOS 27 and iOS 27 now. Thanks also for passing my forum details on - I filed it as a bug report as requested in the other thread too. I'm just happy the block wasn't permanent and that I have access again. :)
Topic: UI Frameworks SubTopic: UIKit Tags:
2w
Reply to My IP address has been blocked by the forums?
Same problem here - the dev forums now show “403 Forbidden” (I can only post this by turning off wi-fi on my phone). It started just after 5pm GMT today when I tried to create a post that included two screenshots to demonstrate the issue I was seeing. One triggered an error the first time I tried to add it, so I suspect it was the screenshots that triggered the block. I have submitted a bug report with the info requested - FB23421947.
Jun ’26
Reply to How to override NSTextView dragging behaviour without overriding mouseDown:?
NSTextSelectionManager uses the NSTextSelectionDataSource protocol to get the information it needs to communicate selection with the delegate. While these are using TK2 values we were able to have NSLayoutManager also conform to the datasource protocol so it can interact with NSTextSelectionManager. Ah, that's great, thank you. I really appreciate the work still going in to keep TextKit 1 up to date with things like this. I've built a lot of TK2 alternatives to my TK1 code ready to switch one day, and I use TK2 where I can, but for now TK1 remains the only option for some things. (Not that I'm in any rush for TK2 to catch up, as I do not look forward to the day I have to rewrite in TK2 my code that handles laying out multiple pages with footnotes, widow and orphan control and so on.) In Seed 1 NSTextView doesn't use the public NSGestureRecognizerDelegate protocol so you're safe to implement your own gesture recognizers there. We're looking at making additional changes throughout AppKit to ensure that you are able to safely subclass and implement gesture recognisers without future grief for either of us. That's great to know, thanks. I had missed that NSTextView and NSTableView's private implementation of the gesture recogniser delegate methods have underscores before them so don't get in the way. Anyway, thanks again, your answers have been really informative and helpful and I now know how to approach my updates.
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to How to override NSTextView dragging behaviour without overriding mouseDown:?
Thank you for such a fantastic and thorough answer! That covers pretty much everything I was missing and I really appreciate it. You can get to the manager through the new NSView.textSelectionManager property. I had been looking for this but in the wrong place - I was looking in NSTextView and NSTextInputClient and hadn't thought to look on NSView. That's great that this exists. If you're already vending the image through NSTextAttachmentViewProvider, you're in good shape. Unfortunately I'm unable to use NSTextAttachmentViewProvider, as I believe it is TextKit 2 only, whereas for now I need to use TextKit 1 until TK2 catches up a bit more (multiple text containers, table support and so on). (Hmm, actually, is NSTextSelectionManager even used in TextKit 1? I notice its data source methods all use TK2 ranges and locations.) After a quick test in a sample project, though, it looks like your suggestion of using textSelectionManager.gesturesForFailureRequirements should work perfectly. It seems that requireGestureRecognizerToFail: is only available in UIKit, not AppKit, but I was able to achieve the same effect using: - (BOOL)gestureRecognizer:(NSGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(NSGestureRecognizer *)otherGestureRecognizer { return [self.textSelectionManager.gesturesForFailureRequirements containsObject:otherGestureRecognizer]; } One question regarding this though: a class dump reveals that NSTextView already implements several NSGestureRecognizerDelegate methods, which presumably means that if I implement the delegate methods myself in my subclass, I risk breaking standard behaviour. So am I right in thinking that I should avoid making my NSTextView subclass the delegate of my own gesture recogniser? (Is there a reason NSTextView doesn't publicly declare the NSGestureRecognizerDelegate methods it conforms to so that we can override them? I notice it's the same with other views migrating to gesture recognisers, such as NSTableView. Sorry, that was two questions.) The best way to do this currently is providing different subclasses depending on what OS your running on, but ugh. We're still working on a better answer though so stay tuned. This is all really useful information, thanks. Given that I create my text views programmatically, I could use different subclasses, but I'll probably just get gesture recognisers working (so that I'm ready for the transition), and then stick to using mouseDown: for the time being, for backward compatibility. Thanks again!
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
I'm definitely not looking forward to this gesture recognizer transition. I override -mouseDown: -mouseDragged: -mouseUp: etc. in like a million different places. Hope it goes easy and doesn't turn into two weeks of work just treading water. I'm in the same boat--lots of mouseDown: etc overrides. I spent the best part of a week updating code for this in our smaller app; I'm not looking forward to doing the same in our large, flagship app. So far it's taken the combination of a few techniques: For some table views I was able to switch to using action. For custom views it's a matter of switching to gesture recognisers. (E.g. There's a new beginDraggingSession(items:gesture:source:) you can use with the NSPanGestureRecognizer for initiating drags.) For subclasses of Apple views that already have gesture recognisers added to them to replace mouseDown:, things get more complicated since the implementation is all private. In a couple of places I ended up creating a helper object to act as the delegate of my own additional gesture recognisers, so as not to interfere with privately implemented gesture delegate methods on the views themselves. For instance I had to do this to support double-clicking on an NSSplitView divider to evenly size the views. (I wish these delegate methods weren't implemented privately. As it is, you have to assume that anything other than a vanilla NSView has, or will have, gesture recognisers and gesture recogniser delegate methods already implemented privately, and that you could break things if you implement your own gesture recogniser delegate methods.) There are still some headaches left, such as my NSTextView subclass, which overrides mouseDown for handling image resizing. Since mouse down and dragging are now all handled in a private implementation of NSTextSelectionManager, there's no longer any obvious way of doing this. Unfortunately the tech note doesn't take into consideration the need for extending and subclassing existing views such as NSTextView or NSOutlineView.
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Thanks for the reply. I don't think you're supposed to read clickedRow in -tableView:selectionIndexesForProposedSelection:. Unless something has changed in macOS 27 the documentation for clickedRow states: This is true, but in testing, clickedRow does work both in an override of selectRowIndexes(_:byExtendingSelection:) and in the delegate tableView(_:selectionIndexesForProposedSelection:). Also, Apple's own sample code for DragNDropOutlineView, which is linked to in the documentation for clickedRow, demonstrates using clickedRow in tableView(_:shouldSelectRow:), which was the older version of tableView(_:selectionIndexesForProposedSelection:). So if Apple's sample code uses it this way, I figure it should be fine. You should be able to pick up a click change by setting the table view's target-action instead of trying to do it in the delegate methods I mentioned in my original post that using an action in this case unfortunately isn't a good solution. I need to load content in response to a selection change, and an action won't be fired when the selection is changed in ways other than clicking (it won't be fired when you use keyboard navigation, for instance, as you note). And since the selection-did-change delegate method is called before the action is called, I can't implement both, either, because the selection-did-change method would load content in the main editor before I could check in the action that it should load in the other editor. So content would wrongly get loaded in both, or I'd have to revert content in the main editor when the action was clicked, which would not be pretty. The only way for this to work is for me to know if the selection changed (in selection-did-change) because of both a mouse click and an Option press - something that has worked for years. Fortunately, like I say, clickedIndex does seem to work for this - at the moment, at least. (The ideal solution would just be for Apple to make clickedIndex valid during selection-did-change calls.) I'm a bit worried that these AppKit changes might break a bunch of stuff. Hope not. I guess touch screen Macs are coming soon. That's my assumption for why these changes are being made too. It's odd that such sweeping changes were barely mentioned at WWDC, getting only a brief mention in the AppKit video and a tech note, since these changes require quite a lot of work in custom controls, tables and text views, and have broken behaviour that has worked for years. (For instance, in NSTextView, selectedRange now only updates after menuForEvent: and its delegate method are called, rather than before, breaking custom context menus in text views. Hopefully that's a bug, though - I've reported it as such.)
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Actually, referring to the ancient DragNDropOutlineView code linked to from the documentation for NSTableView.clickedRow, it seems that although clickedRow has been reset to -1 by the time tableViewDidChangeSelection(_:) is called, it is available and correct in tableView(_:shouldSelectRow:) and its more modern equivalent, tableView(_:selectionIndexesForProposedSelection:). So a simple solution is to check for clickedRow in one of these delegate methods to see if the selection is changing owing to a click, and then save the information in a property that can be used in the didChange delegate method, like this: private var didClick = false func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { didClick = tableView.clickedRow != -1 && proposedSelectionIndexes.contains(tableView.clickedRow) return proposedSelectionIndexes } func tableViewSelectionDidChange(_ notification: Notification) { // We must reset this because `tableViewSelectionDidChange` could be called without the `proposedSelection` delegate method being called first. defer { didClick = false } if didClick && NSEvent.modifierFlags.contains(.option) { openInOtherEditor() return } openInCurrentEditor() }
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Since you're just trying to get at the state of the modifier flags I appreciate the answer and also that in retrospect I should have made my actual question clear sooner in my post, but my question was about how to check if the selection was changed by a mouse click, not about how to get the state of modifier flags. (I mentioned in my post that getting the modifier flags is not a problem and covered by the Tech Note.) So how do I check if a table or outline view selection change was triggered by a mouse click (as opposed to e.g. keyboard navigation) without relying on currentEvent? In my example, opening content in the other editor should happen only if the selection has changed because the user is pressing Option while mouse-clicking on a row. (We wouldn't want this behaviour while using the arrow keys to select while holding Option, for instance, because Option already has a meaning in this case: selecting the first or last item.) Option-click has a long tradition on macOS of providing alternative actions like this, and it's a handy trick that has been available in my app for years. Note that using NSEvent.pressedMouseButtons doesn't work here, either, because the value of that will be 0 by the time tableViewSelectionDidChange is called. So withoutcurrentEvent, how do I check to see if tableViewSelectionDidChange was triggered by a mouse click? Thank you.
Topic: UI Frameworks SubTopic: AppKit Tags:
Jun ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
What makes copies "slow" isn't the writes; it's the reads, since the data still has to be pulled off the disk so it can be sent "back" to write. That makes sense and does indeed seem to be the reason for the speed after first import (the developer of LibZip said much the same recently when I was asking for more details about how it takes advantage of file cloning). The initial read of the large file takes a while, but after that saving is fast, even on reopening the file (because it is read on open). Anyway, thanks again - this discussion has lead to some nice optimisations in the way I'm working with LibZip as well as working around the save error.
Topic: App & System Services SubTopic: Core OS Tags:
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Everything you've described sounds like you're on the right track. Great, thanks! Interesting. Are you primarily "editing" the contents of the zip file (so you end up modifying the data inside, but don't really change it's overall size or structure)? Cloning is a huge help if you can clone the contents and then modify but if your modifications end up changing the fundamental contents, then I wouldn't expect the difference to be nearly as large. At large scale, this eventually devolves to "bytes moved". Yes, I believe editing a large zip file using LibZip can indeed still be slow on APFS. However, the nice thing is that if a user edits a text file in a (zip) project in our app, only the first save to those edits would have the potential to be slow. After that, until they switched to editing another text file in the project, saving subsequent edits even into a huge zip file would be fast on APFS. (A project created in our app can contain text but also research files such as PDFs, media and images.) This is because, on systems that support cloning, LibZip only rewrites the zip file starting with the first changed entry. And whenever I write changes to the zip file, my code deletes the old entry and then re-adds it with the new data, so that the edited text file becomes the last entry. So, say you have a 5KB text file inside a 500MB zip file and it's the first entry. If you edit that text file, in theory the next save will rewrite the entire 500MB. But from then on, because the new data for that text file is now at the end of the zip file's entries, saving changes to it will cause only 5KB (or however large the text file is after edits) to be rewritten. (I say “in theory” because LibZip seems to be doing something smarter somehow; even if you overwrite the text file at the same position—at the first entry—saves are still a lot faster than they would be writing the entire 500MB out again.) And because only text files are editable in my app, they will drift towards the bottom of the zip file's entries as they are edited. Anyway, thanks again for all the help and getting me back on track!
Topic: App & System Services SubTopic: Core OS Tags:
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Great, thanks again. the broader "context" of your app and user base is really a really important factor It's a writing app that is a simpler version of our flagship app, and we want it as user-friendly as possible. It's therefore a bit of a balancing act in this regard (users could end up with large files because they can import research, but in general we want to hide this sort of stuff from the user as much as possible). Anyway, I think I'm mostly there now. I have it making a clone of the temp file on systems that support cloning (.volumeSupportsFileCloning), attempting a re-save with that, or falling back on a check of the error message and whether the temp and original files have swapped places otherwise. It all seems to be working well so far. However, the one detail I'd be careful about is where you put that temp directory. What's the best way of being careful about this, or do you just mean by using the item replacement directory where possible? As far as I know, there are only two ways of getting a temp directory: FileManager.url(for: .itemReplacementDirectory...) - ensures the temp folder is on the same volume as the passed-in URL. FileManager.temporaryDirectory or URL.temporaryDirectory - places the temp folder in the data volume? Or home directory? (Under sandboxing at least it seems to be in the home directory.) My current solution only uses temporaryDirectory if it supports cloning and the item replacement directory doesn't, otherwise it uses the item replacement directory to be sure that the work is done on the same volume as the file that is being replaced. (LibZip is much faster on a volume that supports cloning, and in most cases re-zipping a large file without cloning is slower than copying the file between volumes for the zip operation.)
Topic: App & System Services SubTopic: Core OS Tags:
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Thanks again! I think "very fast" actually understates how significant the performance difference is. Ha, true. In practice it seems “instant”, to the extent that on APFS, updating huge zip files is not much slower than in-place saving into a package. I don't know if anyone has ever shipped a solution that worked like this, but... it might be worth thinking about using DiskImages as a "file format". Interesting! Although cross-platform compatibility might be an issue here. The "replaceItem(at:...)" documentation actually answers this… Sorry, I should have been more clear, although thinking about it I have been tying myself up in knots and the solution was indeed here all along. I was referring to the circumstances we were discussing before, where we don’t want to do the temp work on the same volume as the destination because the destination volume is slow. In other words, we have deliberately created the temp folder for updating our file on another volume (e.g. one that supports APFS), because the one created using url(for: .itemReplacementDirectory…) would be too slow, and now we need to move that temp file into place on the other volume. From your answer I realise I was overlooking the obvious: after doing the work in the fast temp directory, I then need to create a second temp directory on the slower destination volume using url(for: .itemReplacementDirectory…), copy the file across, and then use replaceItemAt from there. Yes, it will, at least in my testing. More specifically, I modified your test project to this: while(!finishedSave) { _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempURL) This approach wouldn’t work anyway. The nature of this specific error means that you cannot retry replaceItemAt on the same URLs like this, because after the error, savingURL and tempURL have swapped places. So in your sample code, if the second replaceItemAt succeeds, you’ve just replaced the newer version with the older version again, so that the save has effectively done nothing. We’ll only get the result we want when failCount % 2 == 0. You can test this by logging the expected and actual final content of the file (i.e. log the content of tempURL before the loop, and the content of savingURL after it). Whenever failCount % 2 == 1, you’ll end up with old content at the destination, because of the alternate swapping of the original and new files. The other problem with retrying replaceItemAt on the same URLs is that, as you note, tempURL (which after the initial replaceItemAt error contains the older file that was previously in the ubiquitous storage) still has the lock (?) on it which caused the permissions error. So any attempts to use that will continue to fail until the kernel (?) has finished with it. For these reasons, we were previously talking about making a fresh copy of the updated temp file before trying replace, and calling replaceItemAt on that, so that we keep around a valid copy of the new file with which we can try again. (E.g. Have a working copy in the temp dir, update that, clone it, try replace using the clone, if that fails, try again with a fresh clone of the working copy.) To update your code using this sort of approach: var tempCopyURL = tempURL.deletingLastPathComponent().appending(path: UUID().uuidString) var finishedSave = false var failCount = 0 while (!finishedSave) { do { // Create a clone of our new file for replace. try fileManager.copyItem(at: tempURL, to: tempCopyURL) // Try to replace using the clone. _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempCopyURL) try? fileManager.removeItem(at: replacementDirURL) // Clean up. finishedSave = true } catch { failCount += 1 if(failCount == 1) { NSLog("First Fail on \(count-1)") } // Try again on the next pass with a fresh clone. tempCopyURL = tempURL.deletingLastPathComponent().appending(path: UUID().uuidString) } } if(failCount > 0) { NSLog("\(count-1) cleared after \(failCount) retries") } For me, this succeeds on the first retry every time, because we’re working with a fresh temp file, not the one that we’re denied access to. Out of 50,000 saves, I hit the error 150 times and each time it resolved on first retry. (It also ensures we end up with the correct version of the file being moved into place.) The disadvantage of course is that you’re adding in an extra copy of the temp file, which adds overhead on non-APFS/copy-on-write volumes. To return to my original question: I’m curious though as to whether the bug could occur twice in immediate succession, so that the resave also triggers the error. Here I was wondering whether we could, on rare occasions, encounter the error twice in immediate succession even with the approach of using a fresh clone of the temp file for each attempt. My suspicion is that this shouldn’t happen, because here’s my wild (and completely uneducated!) guess as to what is happening: Given that this weird error only happens for ubiquitous files, I’m guessing that the problem occurs when the kernel is intermittently doing something cloud-related with the original file, putting some sort of lock on it that prevents us from deleting it - but not from moving it for some reason. replaceItemAt successfully swaps out the original ubiquitous file for the replacement, but the kernel still has a lock on the original file (which is now in the temp folder) and so won’t allow it to be deleted, so replaceItemAt throws an error. So if at this point we immediately retry replaceItemAt with a fresh clone, all should be good because the kernel shouldn’t be doing anything yet with the file that was, in the same run loop, just swapped into the destination URL. (At this point in fact the file at the destination URL and the fresh clone we’re replacing it with are identical.) Does that sound reasonable? Mostly, you'll want .fileResourceIdentifier. fileContentIdentifier is an APFS specific[1] identifier Thank you. I realised my mistake on this late yesterday while testing. So, given all of the above, I think my approach should be: Make a working copy in a temp dir (if destination doesn’t support cloning but local storage does, make the working copy on the local storage): workingCopyURL. On save, update the working copy. Copy the working copy to a folder created using url(for: .itemReplacementDirectory…): tempURL. Use replaceItemAt, replacing destinationURL with tempURL. If replaceItemAt fails, AND isUbiquitous is true for destinationURL, create a fresh copy of the working copy, and try replaceItemAt again with that. (If the file wasn’t ubiquitous, just throw the error.) If replaceItemAt fails the second time, examine the error to check for this very specific bug, and if it all checks out, move on.
Topic: App & System Services SubTopic: Core OS Tags:
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Error scrutiny: struct FileInfo: Equatable { init?(url: URL) { guard let resourceVals = try?url.resourceValues(forKeys: [.fileResourceIdentifierKey, .fileSizeKey]), let fileID = resourceVals.fileResourceIdentifier, let fileSize = resourceVals.fileSize else { return nil } self.fileID = fileID self.fileSize = fileSize } private let fileID: (any NSCopying & NSSecureCoding & NSObjectProtocol) private let fileSize: Int static func == (lhs: ViewController.FileInfo, rhs: ViewController.FileInfo) -> Bool { return lhs.fileSize == rhs.fileSize && lhs.fileID.isEqual(rhs.fileID) } } func isSafeReplaceError(_ error: Error, fileURL: URL, tempURL: URL, oldFileInfo: FileInfo?, oldTempFileInfo: FileInfo?) -> Bool { // Using the file resource IDs and file size, ensure that the temp file and original file have been swapped. guard let oldFileInfo, let oldTempFileInfo, let fileInfo = FileInfo(url: fileURL), let tempFileInfo = FileInfo(url: tempURL), oldFileInfo == tempFileInfo, oldTempFileInfo == fileInfo, tempFileInfo != fileInfo else { return false } let nsError = error as NSError guard // Check this is a permissions error in the Cocoa error domain. nsError.domain == NSCocoaErrorDomain, nsError.code == NSFileWriteNoPermissionError, // Check "NSURL" and "NSFileNewItemLocationKey" keys both point to the file we tried to replace. let errorURL = nsError.userInfo[NSURLErrorKey] as? URL, let newItemURL = nsError.userInfo["NSFileNewItemLocationKey"] as? URL, errorURL.path(percentEncoded: false) == newItemURL.path(percentEncoded: false), newItemURL.path(percentEncoded: false) == fileURL.path(percentEncoded: false), // Check "NSFileOriginalItemLocationKey" and "NSFileBackupItemLeftBehindLocationKey" both point to the temp file. let originalURL = nsError.userInfo["NSFileOriginalItemLocationKey"] as? URL, let leftBehindURL = nsError.userInfo["NSFileBackupItemLeftBehindLocationKey"] as? URL, originalURL.path(percentEncoded: false) == leftBehindURL.path(percentEncoded: false), originalURL.path(percentEncoded: false) == tempURL.path(percentEncoded: false), // Ensure there is only a single underlying error. nsError.underlyingErrors.count == 1 else { return false } // Now get the underlying error. let underlyingError = nsError.underlyingErrors[0] as NSError guard // Check the underlying error is also a permissions error in the Cocoa domain. underlyingError.domain == NSCocoaErrorDomain, underlyingError.code == NSFileWriteNoPermissionError, // And ensure the the error is with the temp file. let underlyingErrorURL = underlyingError.userInfo[NSURLErrorKey] as? URL, underlyingErrorURL.path(percentEncoded: false) == tempURL.path(percentEncoded: false), // Ensure the underlying error also has a single underlying error. underlyingError.underlyingErrors.count == 1 else { return false } // Now get the underlying error for the underlying error. This should be a POSIX error with error code 1 ("Operation not permitted"). let rootError = underlyingError.underlyingErrors[0] as NSError return rootError.domain == NSPOSIXErrorDomain && rootError.code == 1 }
Topic: App & System Services SubTopic: Core OS Tags:
Mar ’26
Reply to UIDocumentViewController missing page background in browser on iPadOS 27
Many thanks for the reply. That's great to know the engineering team have the UIDocumentViewController bug in hand. I'll definitely be testing each new version as I'm full steam ahead on macOS 27 and iOS 27 now. Thanks also for passing my forum details on - I filed it as a bug report as requested in the other thread too. I'm just happy the block wasn't permanent and that I have access again. :)
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
2w
Reply to My IP address has been blocked by the forums?
Same problem here - the dev forums now show “403 Forbidden” (I can only post this by turning off wi-fi on my phone). It started just after 5pm GMT today when I tried to create a post that included two screenshots to demonstrate the issue I was seeing. One triggered an error the first time I tried to add it, so I suspect it was the screenshots that triggered the block. I have submitted a bug report with the info requested - FB23421947.
Replies
Boosts
Views
Activity
Jun ’26
Reply to How to override NSTextView dragging behaviour without overriding mouseDown:?
NSTextSelectionManager uses the NSTextSelectionDataSource protocol to get the information it needs to communicate selection with the delegate. While these are using TK2 values we were able to have NSLayoutManager also conform to the datasource protocol so it can interact with NSTextSelectionManager. Ah, that's great, thank you. I really appreciate the work still going in to keep TextKit 1 up to date with things like this. I've built a lot of TK2 alternatives to my TK1 code ready to switch one day, and I use TK2 where I can, but for now TK1 remains the only option for some things. (Not that I'm in any rush for TK2 to catch up, as I do not look forward to the day I have to rewrite in TK2 my code that handles laying out multiple pages with footnotes, widow and orphan control and so on.) In Seed 1 NSTextView doesn't use the public NSGestureRecognizerDelegate protocol so you're safe to implement your own gesture recognizers there. We're looking at making additional changes throughout AppKit to ensure that you are able to safely subclass and implement gesture recognisers without future grief for either of us. That's great to know, thanks. I had missed that NSTextView and NSTableView's private implementation of the gesture recogniser delegate methods have underscores before them so don't get in the way. Anyway, thanks again, your answers have been really informative and helpful and I now know how to approach my updates.
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTextView.menuForEvent - getting the affected range on macOS 27
That's brilliant news, thank you!
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to How to override NSTextView dragging behaviour without overriding mouseDown:?
Thank you for such a fantastic and thorough answer! That covers pretty much everything I was missing and I really appreciate it. You can get to the manager through the new NSView.textSelectionManager property. I had been looking for this but in the wrong place - I was looking in NSTextView and NSTextInputClient and hadn't thought to look on NSView. That's great that this exists. If you're already vending the image through NSTextAttachmentViewProvider, you're in good shape. Unfortunately I'm unable to use NSTextAttachmentViewProvider, as I believe it is TextKit 2 only, whereas for now I need to use TextKit 1 until TK2 catches up a bit more (multiple text containers, table support and so on). (Hmm, actually, is NSTextSelectionManager even used in TextKit 1? I notice its data source methods all use TK2 ranges and locations.) After a quick test in a sample project, though, it looks like your suggestion of using textSelectionManager.gesturesForFailureRequirements should work perfectly. It seems that requireGestureRecognizerToFail: is only available in UIKit, not AppKit, but I was able to achieve the same effect using: - (BOOL)gestureRecognizer:(NSGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(NSGestureRecognizer *)otherGestureRecognizer { return [self.textSelectionManager.gesturesForFailureRequirements containsObject:otherGestureRecognizer]; } One question regarding this though: a class dump reveals that NSTextView already implements several NSGestureRecognizerDelegate methods, which presumably means that if I implement the delegate methods myself in my subclass, I risk breaking standard behaviour. So am I right in thinking that I should avoid making my NSTextView subclass the delegate of my own gesture recogniser? (Is there a reason NSTextView doesn't publicly declare the NSGestureRecognizerDelegate methods it conforms to so that we can override them? I notice it's the same with other views migrating to gesture recognisers, such as NSTableView. Sorry, that was two questions.) The best way to do this currently is providing different subclasses depending on what OS your running on, but ugh. We're still working on a better answer though so stay tuned. This is all really useful information, thanks. Given that I create my text views programmatically, I could use different subclasses, but I'll probably just get gesture recognisers working (so that I'm ready for the transition), and then stick to using mouseDown: for the time being, for backward compatibility. Thanks again!
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
I'm definitely not looking forward to this gesture recognizer transition. I override -mouseDown: -mouseDragged: -mouseUp: etc. in like a million different places. Hope it goes easy and doesn't turn into two weeks of work just treading water. I'm in the same boat--lots of mouseDown: etc overrides. I spent the best part of a week updating code for this in our smaller app; I'm not looking forward to doing the same in our large, flagship app. So far it's taken the combination of a few techniques: For some table views I was able to switch to using action. For custom views it's a matter of switching to gesture recognisers. (E.g. There's a new beginDraggingSession(items:gesture:source:) you can use with the NSPanGestureRecognizer for initiating drags.) For subclasses of Apple views that already have gesture recognisers added to them to replace mouseDown:, things get more complicated since the implementation is all private. In a couple of places I ended up creating a helper object to act as the delegate of my own additional gesture recognisers, so as not to interfere with privately implemented gesture delegate methods on the views themselves. For instance I had to do this to support double-clicking on an NSSplitView divider to evenly size the views. (I wish these delegate methods weren't implemented privately. As it is, you have to assume that anything other than a vanilla NSView has, or will have, gesture recognisers and gesture recogniser delegate methods already implemented privately, and that you could break things if you implement your own gesture recogniser delegate methods.) There are still some headaches left, such as my NSTextView subclass, which overrides mouseDown for handling image resizing. Since mouse down and dragging are now all handled in a private implementation of NSTextSelectionManager, there's no longer any obvious way of doing this. Unfortunately the tech note doesn't take into consideration the need for extending and subclassing existing views such as NSTextView or NSOutlineView.
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Thanks for the reply. I don't think you're supposed to read clickedRow in -tableView:selectionIndexesForProposedSelection:. Unless something has changed in macOS 27 the documentation for clickedRow states: This is true, but in testing, clickedRow does work both in an override of selectRowIndexes(_:byExtendingSelection:) and in the delegate tableView(_:selectionIndexesForProposedSelection:). Also, Apple's own sample code for DragNDropOutlineView, which is linked to in the documentation for clickedRow, demonstrates using clickedRow in tableView(_:shouldSelectRow:), which was the older version of tableView(_:selectionIndexesForProposedSelection:). So if Apple's sample code uses it this way, I figure it should be fine. You should be able to pick up a click change by setting the table view's target-action instead of trying to do it in the delegate methods I mentioned in my original post that using an action in this case unfortunately isn't a good solution. I need to load content in response to a selection change, and an action won't be fired when the selection is changed in ways other than clicking (it won't be fired when you use keyboard navigation, for instance, as you note). And since the selection-did-change delegate method is called before the action is called, I can't implement both, either, because the selection-did-change method would load content in the main editor before I could check in the action that it should load in the other editor. So content would wrongly get loaded in both, or I'd have to revert content in the main editor when the action was clicked, which would not be pretty. The only way for this to work is for me to know if the selection changed (in selection-did-change) because of both a mouse click and an Option press - something that has worked for years. Fortunately, like I say, clickedIndex does seem to work for this - at the moment, at least. (The ideal solution would just be for Apple to make clickedIndex valid during selection-did-change calls.) I'm a bit worried that these AppKit changes might break a bunch of stuff. Hope not. I guess touch screen Macs are coming soon. That's my assumption for why these changes are being made too. It's odd that such sweeping changes were barely mentioned at WWDC, getting only a brief mention in the AppKit video and a tech note, since these changes require quite a lot of work in custom controls, tables and text views, and have broken behaviour that has worked for years. (For instance, in NSTextView, selectedRange now only updates after menuForEvent: and its delegate method are called, rather than before, breaking custom context menus in text views. Hopefully that's a bug, though - I've reported it as such.)
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Actually, referring to the ancient DragNDropOutlineView code linked to from the documentation for NSTableView.clickedRow, it seems that although clickedRow has been reset to -1 by the time tableViewDidChangeSelection(_:) is called, it is available and correct in tableView(_:shouldSelectRow:) and its more modern equivalent, tableView(_:selectionIndexesForProposedSelection:). So a simple solution is to check for clickedRow in one of these delegate methods to see if the selection is changing owing to a click, and then save the information in a property that can be used in the didChange delegate method, like this: private var didClick = false func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { didClick = tableView.clickedRow != -1 && proposedSelectionIndexes.contains(tableView.clickedRow) return proposedSelectionIndexes } func tableViewSelectionDidChange(_ notification: Notification) { // We must reset this because `tableViewSelectionDidChange` could be called without the `proposedSelection` delegate method being called first. defer { didClick = false } if didClick && NSEvent.modifierFlags.contains(.option) { openInOtherEditor() return } openInCurrentEditor() }
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTableView: checking for mouse-driven selection changes on macOS 27
Since you're just trying to get at the state of the modifier flags I appreciate the answer and also that in retrospect I should have made my actual question clear sooner in my post, but my question was about how to check if the selection was changed by a mouse click, not about how to get the state of modifier flags. (I mentioned in my post that getting the modifier flags is not a problem and covered by the Tech Note.) So how do I check if a table or outline view selection change was triggered by a mouse click (as opposed to e.g. keyboard navigation) without relying on currentEvent? In my example, opening content in the other editor should happen only if the selection has changed because the user is pressing Option while mouse-clicking on a row. (We wouldn't want this behaviour while using the arrow keys to select while holding Option, for instance, because Option already has a meaning in this case: selecting the first or last item.) Option-click has a long tradition on macOS of providing alternative actions like this, and it's a handy trick that has been available in my app for years. Note that using NSEvent.pressedMouseButtons doesn't work here, either, because the value of that will be 0 by the time tableViewSelectionDidChange is called. So withoutcurrentEvent, how do I check to see if tableViewSelectionDidChange was triggered by a mouse click? Thank you.
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jun ’26
Reply to NSTextAttachment.character symbol suddenly not available anymore resulting in compiler error
I've also reported this as #FB22447417. This seems like a bizarre regression. You now have to use NSAttachmentCharacter (the same as in Objective-C), but NSAttachmentCharacter was renamed to NSTextAttachment.character in Swift 4.2.
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Apr ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
What makes copies "slow" isn't the writes; it's the reads, since the data still has to be pulled off the disk so it can be sent "back" to write. That makes sense and does indeed seem to be the reason for the speed after first import (the developer of LibZip said much the same recently when I was asking for more details about how it takes advantage of file cloning). The initial read of the large file takes a while, but after that saving is fast, even on reopening the file (because it is read on open). Anyway, thanks again - this discussion has lead to some nice optimisations in the way I'm working with LibZip as well as working around the save error.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Everything you've described sounds like you're on the right track. Great, thanks! Interesting. Are you primarily "editing" the contents of the zip file (so you end up modifying the data inside, but don't really change it's overall size or structure)? Cloning is a huge help if you can clone the contents and then modify but if your modifications end up changing the fundamental contents, then I wouldn't expect the difference to be nearly as large. At large scale, this eventually devolves to "bytes moved". Yes, I believe editing a large zip file using LibZip can indeed still be slow on APFS. However, the nice thing is that if a user edits a text file in a (zip) project in our app, only the first save to those edits would have the potential to be slow. After that, until they switched to editing another text file in the project, saving subsequent edits even into a huge zip file would be fast on APFS. (A project created in our app can contain text but also research files such as PDFs, media and images.) This is because, on systems that support cloning, LibZip only rewrites the zip file starting with the first changed entry. And whenever I write changes to the zip file, my code deletes the old entry and then re-adds it with the new data, so that the edited text file becomes the last entry. So, say you have a 5KB text file inside a 500MB zip file and it's the first entry. If you edit that text file, in theory the next save will rewrite the entire 500MB. But from then on, because the new data for that text file is now at the end of the zip file's entries, saving changes to it will cause only 5KB (or however large the text file is after edits) to be rewritten. (I say “in theory” because LibZip seems to be doing something smarter somehow; even if you overwrite the text file at the same position—at the first entry—saves are still a lot faster than they would be writing the entire 500MB out again.) And because only text files are editable in my app, they will drift towards the bottom of the zip file's entries as they are edited. Anyway, thanks again for all the help and getting me back on track!
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Great, thanks again. the broader "context" of your app and user base is really a really important factor It's a writing app that is a simpler version of our flagship app, and we want it as user-friendly as possible. It's therefore a bit of a balancing act in this regard (users could end up with large files because they can import research, but in general we want to hide this sort of stuff from the user as much as possible). Anyway, I think I'm mostly there now. I have it making a clone of the temp file on systems that support cloning (.volumeSupportsFileCloning), attempting a re-save with that, or falling back on a check of the error message and whether the temp and original files have swapped places otherwise. It all seems to be working well so far. However, the one detail I'd be careful about is where you put that temp directory. What's the best way of being careful about this, or do you just mean by using the item replacement directory where possible? As far as I know, there are only two ways of getting a temp directory: FileManager.url(for: .itemReplacementDirectory...) - ensures the temp folder is on the same volume as the passed-in URL. FileManager.temporaryDirectory or URL.temporaryDirectory - places the temp folder in the data volume? Or home directory? (Under sandboxing at least it seems to be in the home directory.) My current solution only uses temporaryDirectory if it supports cloning and the item replacement directory doesn't, otherwise it uses the item replacement directory to be sure that the work is done on the same volume as the file that is being replaced. (LibZip is much faster on a volume that supports cloning, and in most cases re-zipping a large file without cloning is slower than copying the file between volumes for the zip operation.)
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Thanks again! I think "very fast" actually understates how significant the performance difference is. Ha, true. In practice it seems “instant”, to the extent that on APFS, updating huge zip files is not much slower than in-place saving into a package. I don't know if anyone has ever shipped a solution that worked like this, but... it might be worth thinking about using DiskImages as a "file format". Interesting! Although cross-platform compatibility might be an issue here. The "replaceItem(at:...)" documentation actually answers this… Sorry, I should have been more clear, although thinking about it I have been tying myself up in knots and the solution was indeed here all along. I was referring to the circumstances we were discussing before, where we don’t want to do the temp work on the same volume as the destination because the destination volume is slow. In other words, we have deliberately created the temp folder for updating our file on another volume (e.g. one that supports APFS), because the one created using url(for: .itemReplacementDirectory…) would be too slow, and now we need to move that temp file into place on the other volume. From your answer I realise I was overlooking the obvious: after doing the work in the fast temp directory, I then need to create a second temp directory on the slower destination volume using url(for: .itemReplacementDirectory…), copy the file across, and then use replaceItemAt from there. Yes, it will, at least in my testing. More specifically, I modified your test project to this: while(!finishedSave) { _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempURL) This approach wouldn’t work anyway. The nature of this specific error means that you cannot retry replaceItemAt on the same URLs like this, because after the error, savingURL and tempURL have swapped places. So in your sample code, if the second replaceItemAt succeeds, you’ve just replaced the newer version with the older version again, so that the save has effectively done nothing. We’ll only get the result we want when failCount % 2 == 0. You can test this by logging the expected and actual final content of the file (i.e. log the content of tempURL before the loop, and the content of savingURL after it). Whenever failCount % 2 == 1, you’ll end up with old content at the destination, because of the alternate swapping of the original and new files. The other problem with retrying replaceItemAt on the same URLs is that, as you note, tempURL (which after the initial replaceItemAt error contains the older file that was previously in the ubiquitous storage) still has the lock (?) on it which caused the permissions error. So any attempts to use that will continue to fail until the kernel (?) has finished with it. For these reasons, we were previously talking about making a fresh copy of the updated temp file before trying replace, and calling replaceItemAt on that, so that we keep around a valid copy of the new file with which we can try again. (E.g. Have a working copy in the temp dir, update that, clone it, try replace using the clone, if that fails, try again with a fresh clone of the working copy.) To update your code using this sort of approach: var tempCopyURL = tempURL.deletingLastPathComponent().appending(path: UUID().uuidString) var finishedSave = false var failCount = 0 while (!finishedSave) { do { // Create a clone of our new file for replace. try fileManager.copyItem(at: tempURL, to: tempCopyURL) // Try to replace using the clone. _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempCopyURL) try? fileManager.removeItem(at: replacementDirURL) // Clean up. finishedSave = true } catch { failCount += 1 if(failCount == 1) { NSLog("First Fail on \(count-1)") } // Try again on the next pass with a fresh clone. tempCopyURL = tempURL.deletingLastPathComponent().appending(path: UUID().uuidString) } } if(failCount > 0) { NSLog("\(count-1) cleared after \(failCount) retries") } For me, this succeeds on the first retry every time, because we’re working with a fresh temp file, not the one that we’re denied access to. Out of 50,000 saves, I hit the error 150 times and each time it resolved on first retry. (It also ensures we end up with the correct version of the file being moved into place.) The disadvantage of course is that you’re adding in an extra copy of the temp file, which adds overhead on non-APFS/copy-on-write volumes. To return to my original question: I’m curious though as to whether the bug could occur twice in immediate succession, so that the resave also triggers the error. Here I was wondering whether we could, on rare occasions, encounter the error twice in immediate succession even with the approach of using a fresh clone of the temp file for each attempt. My suspicion is that this shouldn’t happen, because here’s my wild (and completely uneducated!) guess as to what is happening: Given that this weird error only happens for ubiquitous files, I’m guessing that the problem occurs when the kernel is intermittently doing something cloud-related with the original file, putting some sort of lock on it that prevents us from deleting it - but not from moving it for some reason. replaceItemAt successfully swaps out the original ubiquitous file for the replacement, but the kernel still has a lock on the original file (which is now in the temp folder) and so won’t allow it to be deleted, so replaceItemAt throws an error. So if at this point we immediately retry replaceItemAt with a fresh clone, all should be good because the kernel shouldn’t be doing anything yet with the file that was, in the same run loop, just swapped into the destination URL. (At this point in fact the file at the destination URL and the fresh clone we’re replacing it with are identical.) Does that sound reasonable? Mostly, you'll want .fileResourceIdentifier. fileContentIdentifier is an APFS specific[1] identifier Thank you. I realised my mistake on this late yesterday while testing. So, given all of the above, I think my approach should be: Make a working copy in a temp dir (if destination doesn’t support cloning but local storage does, make the working copy on the local storage): workingCopyURL. On save, update the working copy. Copy the working copy to a folder created using url(for: .itemReplacementDirectory…): tempURL. Use replaceItemAt, replacing destinationURL with tempURL. If replaceItemAt fails, AND isUbiquitous is true for destinationURL, create a fresh copy of the working copy, and try replaceItemAt again with that. (If the file wasn’t ubiquitous, just throw the error.) If replaceItemAt fails the second time, examine the error to check for this very specific bug, and if it all checks out, move on.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Mar ’26
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Error scrutiny: struct FileInfo: Equatable { init?(url: URL) { guard let resourceVals = try?url.resourceValues(forKeys: [.fileResourceIdentifierKey, .fileSizeKey]), let fileID = resourceVals.fileResourceIdentifier, let fileSize = resourceVals.fileSize else { return nil } self.fileID = fileID self.fileSize = fileSize } private let fileID: (any NSCopying & NSSecureCoding & NSObjectProtocol) private let fileSize: Int static func == (lhs: ViewController.FileInfo, rhs: ViewController.FileInfo) -> Bool { return lhs.fileSize == rhs.fileSize && lhs.fileID.isEqual(rhs.fileID) } } func isSafeReplaceError(_ error: Error, fileURL: URL, tempURL: URL, oldFileInfo: FileInfo?, oldTempFileInfo: FileInfo?) -> Bool { // Using the file resource IDs and file size, ensure that the temp file and original file have been swapped. guard let oldFileInfo, let oldTempFileInfo, let fileInfo = FileInfo(url: fileURL), let tempFileInfo = FileInfo(url: tempURL), oldFileInfo == tempFileInfo, oldTempFileInfo == fileInfo, tempFileInfo != fileInfo else { return false } let nsError = error as NSError guard // Check this is a permissions error in the Cocoa error domain. nsError.domain == NSCocoaErrorDomain, nsError.code == NSFileWriteNoPermissionError, // Check "NSURL" and "NSFileNewItemLocationKey" keys both point to the file we tried to replace. let errorURL = nsError.userInfo[NSURLErrorKey] as? URL, let newItemURL = nsError.userInfo["NSFileNewItemLocationKey"] as? URL, errorURL.path(percentEncoded: false) == newItemURL.path(percentEncoded: false), newItemURL.path(percentEncoded: false) == fileURL.path(percentEncoded: false), // Check "NSFileOriginalItemLocationKey" and "NSFileBackupItemLeftBehindLocationKey" both point to the temp file. let originalURL = nsError.userInfo["NSFileOriginalItemLocationKey"] as? URL, let leftBehindURL = nsError.userInfo["NSFileBackupItemLeftBehindLocationKey"] as? URL, originalURL.path(percentEncoded: false) == leftBehindURL.path(percentEncoded: false), originalURL.path(percentEncoded: false) == tempURL.path(percentEncoded: false), // Ensure there is only a single underlying error. nsError.underlyingErrors.count == 1 else { return false } // Now get the underlying error. let underlyingError = nsError.underlyingErrors[0] as NSError guard // Check the underlying error is also a permissions error in the Cocoa domain. underlyingError.domain == NSCocoaErrorDomain, underlyingError.code == NSFileWriteNoPermissionError, // And ensure the the error is with the temp file. let underlyingErrorURL = underlyingError.userInfo[NSURLErrorKey] as? URL, underlyingErrorURL.path(percentEncoded: false) == tempURL.path(percentEncoded: false), // Ensure the underlying error also has a single underlying error. underlyingError.underlyingErrors.count == 1 else { return false } // Now get the underlying error for the underlying error. This should be a POSIX error with error code 1 ("Operation not permitted"). let rootError = underlyingError.underlyingErrors[0] as NSError return rootError.domain == NSPOSIXErrorDomain && rootError.code == 1 }
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Mar ’26