Post

Replies

Boosts

Views

Activity

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:
9h
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:
6d
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:
1w
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:
2w
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:
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Thanks again for the reply, and especially for such a thorough and helpful one! For example, I suspect this doesn't happen if you start with a security-scoped bookmark, which you resolve to a bookmark before each save. Out of curiosity I just tested this, and I still see the bug. To see it yourself, just use the code from my first post but change the savingURL accessor to use a security-scoped bookmark, as follows: private var bookmarkData: Data? // Ask user to choose this via a save panel. var savingURL: URL? { get { var isStale = false if let bookmarkData, let url = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) { if isStale { // Should really update the bookmark data here... } return url } else { return nil } } set(newURL) { bookmarkData = try? newURL?.bookmarkData(options: .withSecurityScope) setUpSpamSave() } } Then add the following to the top of spamSave() after checking savingURL is non-nil: let didAccess = savingURL.startAccessingSecurityScopedResource() if didAccess == false { print("Failed to start accessing scoped URL.") } defer { if didAccess { savingURL.stopAccessingSecurityScopedResource() } } The save will fail with the same permissions error every now and then. Just to clarify, are you: a) Copying the file once, modifying it over time, then copying that file back for each save. b) Copying the file prior to each save operation. I suspect you're doing "a" (and it's probably what I would do), but if you're doing "b”, then that changes things a bit. I’m actually doing (b), since this is very fast on copy-on-write volumes such as APFS even for large files. (Copy-to-temp file is almost instant; updating the zip file is super-fast too thanks to LibZip’s support for copy-on-write, meaning it doesn’t recreate the entire zip file; then it's just a matter of moving the updated file back into place using replaceItemAt(_:withItemAt:.) For slower volumes, much like Pages.app, we offer a second, package-based version of our file format which supports in-place saving. (Zip-based is the default since it works everywhere and package-based files don’t work with cloud-based services other than iCloud Drive on iOS. But if saves get particularly slow, we prompt users to consider the package-based option.) As I mentioned, I keep a snapshot of the zip file from the previous save around (for cases where the user has accidentally deleted the underlying file between saves), and I have done some testing and I can indeed re-save successfully using that. That is slow, though, since it has to recreate the entire archive. On APFS it’s faster to make a backup copy of the temp file before using replaceItemAt and then to try with the copy if it fails - that seems to work well. Based on your suggestions, though, I’m going to do a bit of refactoring. Keeping the zip file around in the temp folder and making copies from it for replaceItemAt sounds like a great solution with multiple advantages, and since my custom file wrapper already keeps a reference to the previous snapshot, it wouldn’t be difficult to have it keep a reference to a temp file URL too. Your app copies from the destination to your local storage…. Your app pushes that initial save data to the final target. This is a little off-topic, but in this case - where you are doing the intensive work on your local storage and then pushing back to the slower volume when done - what is the safest way of replacing the original file? The point of FileManager.url(for: . itemReplacementDirectory…) is to return a temp folder on the same volume as the passed-in URL, since replaceItemAt(_:withItemAt:) won’t work if the original and new URLs are on different volumes. The only other way I can think of risks data loss: Delete the original file from the destination. Move the updated file from the local storage to the destination. We could make a temp copy of the original file before (1), but if the volume is slow, that adds back in some of the slowness we’re avoiding by doing work on another volume. My own instincts would be to redo the entire save, but if you want to do this, I would do two things: I’m going to focus on retrying the save. I’m curious though as to whether the bug could occur twice in immediate succession, so that the resave also triggers the error. Although I can’t get this to happen in testing, given that the error seems random, I wonder if it is possible if I ran the test for long enough - a day, say. In that case, I wonder if this approach would work: Save - encounter some sort of save error. Retry the save no matter what the error was. If we get another error, examine the error and if it was caused by the bug, just try deleting the temp file and move on. I’ve attached some code at the end of this post that scrutinises the error to check it matches the one triggered by this bug. (Although I wonder if I should use .fileContentIdentifier instead of .fileResourceIdentifier.) Anyway, thanks again, as I’m very close to a solution now.
Topic: App & System Services SubTopic: Core OS Tags:
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Many thanks for the reply and information, Kevin, much appreciated. Have you filed a bug on this and, if so, what's the bug number? As part of that bug, I'd suggest installing the "iCloud Drive" profile, reproducing the issue a few times, then uploading a sysdiagnose of the failure. See the profile installation instructions for the full details of that process. I hadn’t filed a bug report yet because I had assumed it was something I was doing wrong given that using replaceItem and a temporary folder is presumably a common pattern. I’ll file a report tomorrow - I’m following the iCloud Drive profile instructions you linked to and am now waiting the 24 hours they say I need to wait before I can get the sysdiagnose. Once I have that I’ll file the report along with a sample project. Have you tried retrying the save? That appears to work in my testing, though it may not be a workable solution in your case. Beyond that, I'd need a better understanding of exactly how you're interacting with the files and what your full requirements are. With a bit of refactoring I probably could retry the save. In my app this is all done inside my NSDocument’s writeToURL method. I use my own drop-in replacement for FileWrapper (you helped me with some of the finer points of FileWrapper a few years ago) that incrementally writes changes to a zip file using Libzip, which supports incremental saves on copy-on-write systems such as APFS. A potential problem with the re-save approach is that my save usually works by copying the zip file at the original location to a temporary location, updating it there, and then moving it into place using replaceItemAt. After this particular replaceItemAt error, however, the original file has in fact been updated despite the error (the error being on the old version of the file which is now in the temporary directory). So if I re-save by making a copy of that and try updating again, I could potentially mess up the file by trying to save into it stuff that has actually already been done. (However, I do keep a snapshot of the older archive around in case of problems, so I might be able to work around this problem using that.) I wonder, though - given that the original file has in fact been replaced by the temp file despite the error, can I not just check for this and ignore the error if the file seems to have been replaced after all? E.g.: Before replacement, record the file resource ID of the temp file. Use replaceItemAt(originalURL, withItemAt: tempURL). If there’s an error, get the file resource ID for the file at the intended saving location and compare it against the ID I recorded in (1). If they are the same, I know the replacement has succeeded despite the error. In this case, I can just try to delete the temporary folder and move on. If the file IDs of the current user file and the temp file from before replace don’t match or couldn’t be got, attempt a re-save. Is there something wrong with this approach? (I’ve attached some sample code below demonstrating how this might work.) Many thanks, Keith // Get a temporary folder appropriate for creating the new file in. let replacementDirURL = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: savingURL, create: true) // Create the new file at the temporary location. let tempURL = replacementDirURL.appendingPathComponent(savingURL.lastPathComponent) try createNewContentAt(url: tempURL) // Record the file resource ID of the temp file we created. let tempFileID = (try? tempURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier // Now try to move the file into place. do { // Use replaceItemAt to safely replace the original file with the updated file we created at the temp location. _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempURL) // Clean up. try? fileManager.removeItem(at: replacementDirURL) } catch { // Check to see if the original file was in fact replaced despite the error. if let tempFileID, let savingFileID = (try? savingURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier, tempFileID.isEqual(savingFileID) { // If so, just try to remove the temp dir and move on. try? fileManager.removeItem(at: replacementDirURL) } else { // If we got here, replace really did fail and we need to handle it. // We should do some more work and try to resave here before throwing an error. throw error } }
Topic: App & System Services SubTopic: Core OS Tags:
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
An update on this weird behaviour: I have discovered that when replaceItem fails in this circumstance, the temp file has in fact been moved into place correctly and has replaced the original file. But when I get the error, the old original file has taken the place of the old temp file and it's that which cannot be removed. I have tested this by checking both the content and the fileResourceIdentifier of the original file and the temp file, and logging them before and after the error. After the error they are swapped.
Topic: App & System Services SubTopic: Core OS Tags:
2w
Reply to Trying to get UIBarButtonItem custom view to change color within iOS 26 Liquid Glass like native UIBarButtonItem
This may well be related to a bug I'm also seeing in iOS 26. In beta 4, many controls were messed up in dark mode, so that they didn't adapt to a dark appearance at all, including UIBarButtonItems with a customView. There have been some improvements in beta 5, but some controls now have a disabled appearance in dark mode when they are in fact enabled. UIBarButtonItems with a customView are one of the controls affected. I'm seeing this in my own app, and I can easily reproduce this with a UIBarButtonItem that uses a UISegmentedControl as its customView. Here it is in light mode: (Only the link button should be disabled, so all is as expected in light mode.) And in dark mode (or in light mode but against a dark background): As you can see, the segmented control now looks disabled when it isn't. And this is set up using the segmented control directly as the custom view (i.e. UIBarButtonItem(customView: segmentedControl)). I've reported this as FB19431646. The issue may be more general, as nav bar titles in sidebars and inspectors also appear pale grey and disabled (FB19431807). Original report for dark mode issues in beta 4: FB19023069.
Topic: UI Frameworks SubTopic: UIKit Tags:
Aug ’25
Reply to Positioning an image
You should generally add the constraints in your view controller code, where you have access to both views., right after calling addSubview(:). Or if you really want the code in your image view for some reason you could create a method on your image view that takes the view as a parameter (e.g. addToView( view: uIView)) and call that from the view controller.
Topic: UI Frameworks SubTopic: UIKit Tags:
Jul ’25
Reply to Positioning an image
These lines are your problem: self.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true self.topAnchor.constraint(equalTo: self.centerYAnchor).isActive = true You‘re setting self.centerXAnchor to equal itself, which will do nothing, and self.topAnchor to equal self.centerYAnchor, which is impossible. Instead, you want to set self.centerXAnchor to equal the superview’s centerXAnchor and the same for the Y anchor. (In your case the superview will be the view controller’s view.) For a specific position rather than centred, you’ll want to use constraints lining the image view up with the leading/top/trailing/bottom edges of the superview with a constant depending on your needs.
Topic: UI Frameworks SubTopic: UIKit Tags:
Jul ’25
Reply to iOS 26 UISplitViewController in dark mode appearance.
I've also filed a bug report on this just now (#FB19023069). I was trying to work out why a UILabel set as the subview of a glass UIVisualEffectView's content view wasn't adapting to dark mode (not in a Split View), when I realised it wasn't alone. Here's a screenshot of the sample app I sent to Apple: Light mode: So far, I've noticed the following controls are affected: Primary and inspector nav bar titles in a Split View (but not the secondary title). The toggle sidebar button (but not always). Bar button items in the nav bar with isEnabled set to false. Bar button items containing custom views (at least containing a segmented control, such as in my screenshot). Labels inside a UIVisualEffectView using the glass effect. No doubt there's more. Even setting segmentedControl.overrideUserInterfaceStyle = .dark on the segmented control in the custom nav bar item didn't help. It is very strange that Apple's own apps aren't affected.
Topic: UI Frameworks SubTopic: UIKit Tags:
Jul ’25
Reply to Set edge effect style in AppKit
It's strongly implied in the video "Build an AppKit app with the new design": The scroll edge effect is applied automatically underneath toolbar items, title bar accessories, and… split item accessories. I've filed feedback asking for a property to control it programmatically too though. At the moment the automatic detection isn't reliable - there are easily reproducible cases where you get blurring at the top of a scroll view ("soft" effect) when it should be a hard effect (I've filed a couple of bug reports on this). Of course macOS 26 is still in early beta so fingers crossed these bugs will be fixed, but it would be good to have some manual control in case there are edge cases that automatic detection still misses - especially since there are properties for this in both SwiftUI and UIKit, with only AppKit lacking.
Topic: UI Frameworks SubTopic: AppKit Tags:
Jul ’25
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
9h
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
6d
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
1w
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
2w
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
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Thanks again for the reply, and especially for such a thorough and helpful one! For example, I suspect this doesn't happen if you start with a security-scoped bookmark, which you resolve to a bookmark before each save. Out of curiosity I just tested this, and I still see the bug. To see it yourself, just use the code from my first post but change the savingURL accessor to use a security-scoped bookmark, as follows: private var bookmarkData: Data? // Ask user to choose this via a save panel. var savingURL: URL? { get { var isStale = false if let bookmarkData, let url = try? URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) { if isStale { // Should really update the bookmark data here... } return url } else { return nil } } set(newURL) { bookmarkData = try? newURL?.bookmarkData(options: .withSecurityScope) setUpSpamSave() } } Then add the following to the top of spamSave() after checking savingURL is non-nil: let didAccess = savingURL.startAccessingSecurityScopedResource() if didAccess == false { print("Failed to start accessing scoped URL.") } defer { if didAccess { savingURL.stopAccessingSecurityScopedResource() } } The save will fail with the same permissions error every now and then. Just to clarify, are you: a) Copying the file once, modifying it over time, then copying that file back for each save. b) Copying the file prior to each save operation. I suspect you're doing "a" (and it's probably what I would do), but if you're doing "b”, then that changes things a bit. I’m actually doing (b), since this is very fast on copy-on-write volumes such as APFS even for large files. (Copy-to-temp file is almost instant; updating the zip file is super-fast too thanks to LibZip’s support for copy-on-write, meaning it doesn’t recreate the entire zip file; then it's just a matter of moving the updated file back into place using replaceItemAt(_:withItemAt:.) For slower volumes, much like Pages.app, we offer a second, package-based version of our file format which supports in-place saving. (Zip-based is the default since it works everywhere and package-based files don’t work with cloud-based services other than iCloud Drive on iOS. But if saves get particularly slow, we prompt users to consider the package-based option.) As I mentioned, I keep a snapshot of the zip file from the previous save around (for cases where the user has accidentally deleted the underlying file between saves), and I have done some testing and I can indeed re-save successfully using that. That is slow, though, since it has to recreate the entire archive. On APFS it’s faster to make a backup copy of the temp file before using replaceItemAt and then to try with the copy if it fails - that seems to work well. Based on your suggestions, though, I’m going to do a bit of refactoring. Keeping the zip file around in the temp folder and making copies from it for replaceItemAt sounds like a great solution with multiple advantages, and since my custom file wrapper already keeps a reference to the previous snapshot, it wouldn’t be difficult to have it keep a reference to a temp file URL too. Your app copies from the destination to your local storage…. Your app pushes that initial save data to the final target. This is a little off-topic, but in this case - where you are doing the intensive work on your local storage and then pushing back to the slower volume when done - what is the safest way of replacing the original file? The point of FileManager.url(for: . itemReplacementDirectory…) is to return a temp folder on the same volume as the passed-in URL, since replaceItemAt(_:withItemAt:) won’t work if the original and new URLs are on different volumes. The only other way I can think of risks data loss: Delete the original file from the destination. Move the updated file from the local storage to the destination. We could make a temp copy of the original file before (1), but if the volume is slow, that adds back in some of the slowness we’re avoiding by doing work on another volume. My own instincts would be to redo the entire save, but if you want to do this, I would do two things: I’m going to focus on retrying the save. I’m curious though as to whether the bug could occur twice in immediate succession, so that the resave also triggers the error. Although I can’t get this to happen in testing, given that the error seems random, I wonder if it is possible if I ran the test for long enough - a day, say. In that case, I wonder if this approach would work: Save - encounter some sort of save error. Retry the save no matter what the error was. If we get another error, examine the error and if it was caused by the bug, just try deleting the temp file and move on. I’ve attached some code at the end of this post that scrutinises the error to check it matches the one triggered by this bug. (Although I wonder if I should use .fileContentIdentifier instead of .fileResourceIdentifier.) Anyway, thanks again, as I’m very close to a solution now.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Just to add that I have now filed the bug as #FB22107069.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
Many thanks for the reply and information, Kevin, much appreciated. Have you filed a bug on this and, if so, what's the bug number? As part of that bug, I'd suggest installing the "iCloud Drive" profile, reproducing the issue a few times, then uploading a sysdiagnose of the failure. See the profile installation instructions for the full details of that process. I hadn’t filed a bug report yet because I had assumed it was something I was doing wrong given that using replaceItem and a temporary folder is presumably a common pattern. I’ll file a report tomorrow - I’m following the iCloud Drive profile instructions you linked to and am now waiting the 24 hours they say I need to wait before I can get the sysdiagnose. Once I have that I’ll file the report along with a sample project. Have you tried retrying the save? That appears to work in my testing, though it may not be a workable solution in your case. Beyond that, I'd need a better understanding of exactly how you're interacting with the files and what your full requirements are. With a bit of refactoring I probably could retry the save. In my app this is all done inside my NSDocument’s writeToURL method. I use my own drop-in replacement for FileWrapper (you helped me with some of the finer points of FileWrapper a few years ago) that incrementally writes changes to a zip file using Libzip, which supports incremental saves on copy-on-write systems such as APFS. A potential problem with the re-save approach is that my save usually works by copying the zip file at the original location to a temporary location, updating it there, and then moving it into place using replaceItemAt. After this particular replaceItemAt error, however, the original file has in fact been updated despite the error (the error being on the old version of the file which is now in the temporary directory). So if I re-save by making a copy of that and try updating again, I could potentially mess up the file by trying to save into it stuff that has actually already been done. (However, I do keep a snapshot of the older archive around in case of problems, so I might be able to work around this problem using that.) I wonder, though - given that the original file has in fact been replaced by the temp file despite the error, can I not just check for this and ignore the error if the file seems to have been replaced after all? E.g.: Before replacement, record the file resource ID of the temp file. Use replaceItemAt(originalURL, withItemAt: tempURL). If there’s an error, get the file resource ID for the file at the intended saving location and compare it against the ID I recorded in (1). If they are the same, I know the replacement has succeeded despite the error. In this case, I can just try to delete the temporary folder and move on. If the file IDs of the current user file and the temp file from before replace don’t match or couldn’t be got, attempt a re-save. Is there something wrong with this approach? (I’ve attached some sample code below demonstrating how this might work.) Many thanks, Keith // Get a temporary folder appropriate for creating the new file in. let replacementDirURL = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: savingURL, create: true) // Create the new file at the temporary location. let tempURL = replacementDirURL.appendingPathComponent(savingURL.lastPathComponent) try createNewContentAt(url: tempURL) // Record the file resource ID of the temp file we created. let tempFileID = (try? tempURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier // Now try to move the file into place. do { // Use replaceItemAt to safely replace the original file with the updated file we created at the temp location. _ = try fileManager.replaceItemAt(savingURL, withItemAt: tempURL) // Clean up. try? fileManager.removeItem(at: replacementDirURL) } catch { // Check to see if the original file was in fact replaced despite the error. if let tempFileID, let savingFileID = (try? savingURL.resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier, tempFileID.isEqual(savingFileID) { // If so, just try to remove the temp dir and move on. try? fileManager.removeItem(at: replacementDirURL) } else { // If we got here, replace really did fail and we need to handle it. // We should do some more work and try to resave here before throwing an error. throw error } }
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
2w
Reply to FileManager.replaceItemAt(_:withItemAt:) fails sporadically on ubiquitous items
An update on this weird behaviour: I have discovered that when replaceItem fails in this circumstance, the temp file has in fact been moved into place correctly and has replaced the original file. But when I get the error, the old original file has taken the place of the old temp file and it's that which cannot be removed. I have tested this by checking both the content and the fileResourceIdentifier of the original file and the temp file, and logging them before and after the error. After the error they are swapped.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
2w
Reply to Trying to get UIBarButtonItem custom view to change color within iOS 26 Liquid Glass like native UIBarButtonItem
[Deleted - sorry, I missed the comment from the DTS engineer on my earlier post, so what I posted here was irrelevant, but there doesn't seem to be a way to delete the post. Glad the op got their issue sorted.]
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Aug ’25
Reply to Trying to get UIBarButtonItem custom view to change color within iOS 26 Liquid Glass like native UIBarButtonItem
This may well be related to a bug I'm also seeing in iOS 26. In beta 4, many controls were messed up in dark mode, so that they didn't adapt to a dark appearance at all, including UIBarButtonItems with a customView. There have been some improvements in beta 5, but some controls now have a disabled appearance in dark mode when they are in fact enabled. UIBarButtonItems with a customView are one of the controls affected. I'm seeing this in my own app, and I can easily reproduce this with a UIBarButtonItem that uses a UISegmentedControl as its customView. Here it is in light mode: (Only the link button should be disabled, so all is as expected in light mode.) And in dark mode (or in light mode but against a dark background): As you can see, the segmented control now looks disabled when it isn't. And this is set up using the segmented control directly as the custom view (i.e. UIBarButtonItem(customView: segmentedControl)). I've reported this as FB19431646. The issue may be more general, as nav bar titles in sidebars and inspectors also appear pale grey and disabled (FB19431807). Original report for dark mode issues in beta 4: FB19023069.
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Aug ’25
Reply to Positioning an image
You should generally add the constraints in your view controller code, where you have access to both views., right after calling addSubview(:). Or if you really want the code in your image view for some reason you could create a method on your image view that takes the view as a parameter (e.g. addToView( view: uIView)) and call that from the view controller.
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Jul ’25
Reply to Positioning an image
These lines are your problem: self.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true self.topAnchor.constraint(equalTo: self.centerYAnchor).isActive = true You‘re setting self.centerXAnchor to equal itself, which will do nothing, and self.topAnchor to equal self.centerYAnchor, which is impossible. Instead, you want to set self.centerXAnchor to equal the superview’s centerXAnchor and the same for the Y anchor. (In your case the superview will be the view controller’s view.) For a specific position rather than centred, you’ll want to use constraints lining the image view up with the leading/top/trailing/bottom edges of the superview with a constant depending on your needs.
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Jul ’25
Reply to iOS 26 UISplitViewController in dark mode appearance.
I've also filed a bug report on this just now (#FB19023069). I was trying to work out why a UILabel set as the subview of a glass UIVisualEffectView's content view wasn't adapting to dark mode (not in a Split View), when I realised it wasn't alone. Here's a screenshot of the sample app I sent to Apple: Light mode: So far, I've noticed the following controls are affected: Primary and inspector nav bar titles in a Split View (but not the secondary title). The toggle sidebar button (but not always). Bar button items in the nav bar with isEnabled set to false. Bar button items containing custom views (at least containing a segmented control, such as in my screenshot). Labels inside a UIVisualEffectView using the glass effect. No doubt there's more. Even setting segmentedControl.overrideUserInterfaceStyle = .dark on the segmented control in the custom nav bar item didn't help. It is very strange that Apple's own apps aren't affected.
Topic: UI Frameworks SubTopic: UIKit Tags:
Replies
Boosts
Views
Activity
Jul ’25
Reply to Set edge effect style in AppKit
It's strongly implied in the video "Build an AppKit app with the new design": The scroll edge effect is applied automatically underneath toolbar items, title bar accessories, and… split item accessories. I've filed feedback asking for a property to control it programmatically too though. At the moment the automatic detection isn't reliable - there are easily reproducible cases where you get blurring at the top of a scroll view ("soft" effect) when it should be a hard effect (I've filed a couple of bug reports on this). Of course macOS 26 is still in early beta so fingers crossed these bugs will be fixed, but it would be good to have some manual control in case there are edge cases that automatic detection still misses - especially since there are properties for this in both SwiftUI and UIKit, with only AppKit lacking.
Topic: UI Frameworks SubTopic: AppKit Tags:
Replies
Boosts
Views
Activity
Jul ’25