Until now I was using FileManager.contentsEqual(atPath:andPath:) to compare file contents in my App Store app, but then a user reported that this operation is way slower than just copying the files (which I made faster a while ago, as explained in Making filecopy faster by changing block size).
I thought that maybe the FileManager implementation reads the two files with a small block size, so I implemented a custom comparison with the same block size I use for filecopy (as explained in the linked post), and it runs much faster. When using the code for testing repeatedly also found on that other post, this new implementation is about the same speed as FileManager for 1KB files, but runs 10-20x faster for 1MB files or bigger.
Feel free to comment on my implementation below.
extension FileManager {
func fastContentsEqual(atPath path1: String, andPath path2: String, progress: (_ delta: Int) -> Bool) -> Bool {
do {
let bufferSize = 16_777_216
let sourceDescriptor = open(path1, O_RDONLY | O_NOFOLLOW, 0)
if sourceDescriptor < 0 {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
let sourceFile = FileHandle(fileDescriptor: sourceDescriptor)
let destinationDescriptor = open(path2, O_RDONLY | O_NOFOLLOW, 0)
if destinationDescriptor < 0 {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
let destinationFile = FileHandle(fileDescriptor: destinationDescriptor)
var equal = true
while autoreleasepool(invoking: {
let sourceData = sourceFile.readData(ofLength: bufferSize)
let destinationData = destinationFile.readData(ofLength: bufferSize)
equal = sourceData == destinationData
return sourceData.count > 0 && progress(sourceData.count) && equal
}) { }
if close(sourceDescriptor) < 0 {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
if close(destinationDescriptor) < 0 {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
return equal
} catch {
return contentsEqual(atPath: path1, andPath: path2) // use this as a fallback for unsupported files (like symbolic links)
}
}
}
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
My app supports different plain text file formats, including the standard .txt and Markdown. When creating a new document, my app already asks which format it should have, so when saving it, I would expect that the save panel already selects that format in the popup button, but currently it always selects "Plain Text". For example, I would expect for a Markdown document that it selects "Markdown" instead of "Plain Text".
Is there a way to force it to select the most specific format matching the document format?
For many years I've had the following code to access the active objects of a table view in my App Store app:
class MyViewController: NSViewController: NSMenuItemValidation {
private var tableView: NSTableView!
private var objects = [MyObject]()
func numberOfRows(in tableView: NSTableView) -> Int {
return objects.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
// make view for row
}
private var activeObjects: [MyObject] {
return tableView?.activeRowIndexes.map({ objects[$0] }) ?? []
}
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
let activeObjects = self.activeObjects
...
}
}
extension NSTableView {
var activeRowIndexes: IndexSet {
return clickedRow == -1 || selectedRowIndexes.contains(clickedRow) ? selectedRowIndexes : IndexSet(integer: clickedRow)
}
}
In one of the recent updates, I wanted to add some kind of header to the table view, so I decided to add a row at the beginning and offset the indexes by 1.
func numberOfRows(in tableView: NSTableView) -> Int {
return objects.count + 1
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if row == 0 {
// make header view
} else {
// make view for row - 1
}
}
private var activeObjects: [MyObject] {
return tableView?.activeRowIndexes.subtracting(IndexSet(integer: 0)).map({ objects[$0 - 1] }) ?? []
}
But since I added this change, Xcode regularly downloads crash reports from clients crashing during menu item validation in IndexSet.map with reason Code 5 Trace/BPT trap: 5. I assumed that I was accessing an invalid array index, so I added some debug code: the crash report would then show the invalid index beside the crashed thread's name.
private var activeObjects: [MyObject] {
return tableView?.activeRowIndexes.subtracting(IndexSet(integer: 0)).map({ i in
if !objects.indices.contains(i - 1) {
Thread.current.name = (Thread.current.name ?? "") + ". Invalid index \(i - 1) for count \(objects.count)"
preconditionFailure()
}
return objects[i - 1]
}) ?? []
}
But the crash reports for this new app version look just like the old ones and the thread name is not changed. Indeed, when recreating an invalid index access on my Mac, the crash report mentions Array._checkSubscript(_:wasNativeTypeChecked:), which does not appear in the crash reports downloaded by Xcode.
Manually symbolicating the crash report also doesn't give any more information: all lines referring to my app code are resolved to either /<compiler-generated>:0 or MyViewController.swift:0.
Apparently the problem is not an invalid array index, but something else. Does anybody have a clue what the problem could be?
(Note: the crash report mentions Sequence.compactMap because now I'm effectively calling tableView?.activeRowIndexes.compactMap, but the same crash happened before when calling IndexSet.map, which would appear in the crash report as Collection.map.)
crash2.crash
In my app the user can select a source folder to be synced with a destination folder. The sync can also happen in response to a change in the source folder detected with FSEventStreamCreate.
If the user unzips an archive in the source folder and the sync process begins before the unzip operation has completed, the sync can fail because of a "Permission denied" error. I assume this is related to the posix permissions of the extracted folder being 420 during the unzip operation and (in my case) 511 afterwards.
Is there a way to detect than an unzip operation is in progress and wait until it has completed? I thought that using NSFileCoordinator would solve this issue, but unfortunately it's not the case. Since an unzip operation can last any amount of time, it's not ideal to just delay a sync by a fixed number of seconds and let the user deal with any error if the unzip operation takes longer.
let openPanel = NSOpenPanel()
openPanel.canChooseDirectories = true
if openPanel.runModal() == .cancel {
return
}
let url = openPanel.urls[0].appendingPathComponent("extracted", isDirectory: false)
var error: NSError?
NSFileCoordinator(filePresenter: nil).coordinate(readingItemAt: url, error: &error) { url in
do {
print(try FileManager.default.attributesOfItem(atPath: url.path).sorted(by: { $0.key.rawValue < $1.key.rawValue }).map({ ($0.key.rawValue, $0.value) }))
try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
} catch {
print(error)
}
}
if let error = error {
print("file coordinator error:", error)
}
In my app I have a background task performed on a custom DispatchQueue. When it has completed, I update the UI in DispatchQueue.main.async. In a particular case, the app then needs to show a modal window that contains a table view, but I have noticed that when scrolling through the tableview, it only responds very slowly.
It appears that this happens when the table view in the modal window is presented in DispatchQueue.main.async. Presenting it in perform(_:with:afterDelay:) or in a Timer.scheduledTimer(withTimeInterval:repeats:block:) on the other hand works. Why? This seems like an ugly workaround.
I created FB7448414 in November 2019 but got no response.
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
let windowController = NSWindowController(window: NSWindow(contentViewController: ViewController()))
// 1. works
// runModal(for: windowController)
// 2. works
// perform(#selector(runModal), with: windowController, afterDelay: 0)
// 3. works
// Timer.scheduledTimer(withTimeInterval: 0, repeats: false) { [self] _ in
// self.runModal(for: windowController)
// }
// 4. doesn't work
DispatchQueue.main.async {
self.runModal(for: windowController)
}
}
@objc func runModal(for windowController: NSWindowController) {
NSApp.runModal(for: windowController.window!)
}
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
override func loadView() {
let tableView = NSTableView()
tableView.dataSource = self
tableView.delegate = self
tableView.addTableColumn(NSTableColumn())
let scrollView = NSScrollView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
scrollView.documentView = tableView
scrollView.hasVerticalScroller = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
view = scrollView
}
func numberOfRows(in tableView: NSTableView) -> Int {
return 100
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let view = NSTableCellView()
let textField = NSTextField(labelWithString: "\(row)")
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textField)
NSLayoutConstraint.activate([textField.leadingAnchor.constraint(equalTo: view.leadingAnchor), textField.trailingAnchor.constraint(equalTo: view.trailingAnchor), textField.topAnchor.constraint(equalTo: view.topAnchor), textField.bottomAnchor.constraint(equalTo: view.bottomAnchor)])
return view
}
}
I noticed that sometimes TextKit2 decides to crop some text instead of soft-wrapping it to the next line.
This can be reproduced by running the code below, then resizing the window by dragging the right margin to the right until you see the text with green background (starting with “file0”) at the end of the first line.
If you now slowly move the window margin back to the left, you’ll see that for some time that green “file0” text is cropped and so is the end of the text with red background, until at some point it is soft-wrapped on the second line.
I just created FB18289242. Is there a workaround?
class ViewController: NSViewController {
override func loadView() {
let textView = NSTextView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
let string = NSMutableAttributedString(string: "file0\t143548282\t1970-01-01T00:00:00Z\t1\t1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75", attributes: [.foregroundColor: NSColor.labelColor, .backgroundColor: NSColor.red.withAlphaComponent(0.2)])
string.append(NSAttributedString(string: "file0\t143548290\t1970-01-01T00:05:00Z\t 2\t0f6460d0ed7825fed6bda0f4d9c14942d88edc7ff236479212e69f081815e6f1742c272753b77cc6437f06ef93a46271c6ff9513c68945075212434080e60c82", attributes: [.foregroundColor: NSColor.labelColor, .backgroundColor: NSColor.green.withAlphaComponent(0.2)]))
textView.textContentStorage!.textStorage!.setAttributedString(string)
textView.autoresizingMask = [.width, .height]
let scrollView = NSScrollView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
view = scrollView
}
}
In 2020 I created FB7719215, which I updated several times (including just now) and in 2021 I created FB9204092, but the issue is still there: when I keep Xcode open (currently version 16.3), my battery drains much quicker, even when it's apparently idle. For instance, today I barely did anything in Xcode, but still it has been at a constant 90% CPU for the last hours, and I keep checking the battery percentage to check how much time I have left.
Does anyone at Apple has an explanation, workaround and/or fix?
I'm trying to copy some files from the Finder on macOS 14 to several Simulator instances running iOS 16 and 17. When I right-click the file in the Finder, I can select Share > Simulator, then a share dialog pops up where I select the relevant Simulator and click on Send. According to this official help topic, the Files app should open allowing me to choose the destination, but instead nothing happens and the shared file is nothing to be seen. What's the issue?
When I launch the Quick Look Preview Extension target in Xcode, an app called Quick Look Simulator opens with an almost empty window:
Online I read that the Terminal command qlmanage allows to test Quick Look plugins (which I think were an older format for creating Quick Look extensions), but running
qlmanage -p /path/to/previewed/file -c public.text -g /path/to/QuickLookPreviewExtension.appex
(where QuickLookPreviewExtension.appex is generated by the Xcode build and is located in the DerivedData folder) gives an error
Can't get generator at QuickLookPreviewExtension.appex
How can I debug a Quick Look Preview Extension?
All the threads only contain system calls. The crashed thread only contains a single call to my app's code which is main.swift:13.
What could cause such a crash?
crash.crash
I would like to provide a default filename when saving a document depending on the document data. I thought I could do so by overriding NSDocument.prepareSavePanel(_:) and setting NSSavePanel.nameFieldStringValue, but simply implementing that method seems to hide the file format popup button shown by default (see image). Calling super doesn't help.
Is it possible to set a default filename and keep the file format popup button? On macOS 15, I can toggle NSSavePanel.showsContentTypes, but how about macOS 14 and older?
Xcode has been downloading many similar crash reports for my app for some time now, related to an index out of range runtime exception when accessing a Swift array. The crashes always happen in methods triggered by user input or during menu item validation when I try to access the data source array by using the following code to determine the indexes of the relevant table rows:
let indexes = clickedRow == -1 || selectedRowIndexes.contains(clickedRow) ? selectedRowIndexes : IndexSet(integer: clickedRow)
I was never able to reproduce the crash until today. When the app crashed in the Xcode debugger, I examined the variables clickedRow and selectedRowIndexes.first, which were 1 and 0 respectively. What's interesting: the table view only contained one row, so clickedRow was effectively invalid. I tried to reproduce the issue several times afterwards, but it never happened again.
What could cause this issue? What are the circumstances where it is invalid? Do I always have to explicitly check if clickedRow is within the data source range?
I would like to print a NSTextStorage on multiple pages and add annotations to the side margins corresponding to certain text ranges. For example, for all occurrences of # at the start of a line, the side margin should show an automatically increasing number.
My idea was to create a NSLayoutManager and dynamically add NSTextContainer instances to it until all text is laid out. The layoutManager would then allow me to get the bounding rectangle of the interesting text ranges so that I can draw the corresponding numbers at the same height inside the side margin. This approach works well on macOS, but I'm having some issues on iOS.
When running the code below in an iPad Simulator, I would expect that the print preview shows 3 pages, the first with the numbers 0-1, the second with the numbers 2-3, and the last one with the number 4. Instead the first page shows the number 4, the second one the numbers 2-4, and the last one the numbers 0-4. It's as if the pages are inverted, and each page shows the text starting at the correct location but always ending at the end of the complete text (and not the range assigned to the relative textContainer).
I've created FB17026419.
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
let printController = UIPrintInteractionController.shared
let printPageRenderer = PrintPageRenderer()
printPageRenderer.pageSize = CGSize(width: 100, height: 100)
printPageRenderer.textStorage = NSTextStorage(string: (0..<5).map({ "\($0)" }).joined(separator: "\n"), attributes: [.font: UIFont.systemFont(ofSize: 30)])
printController.printPageRenderer = printPageRenderer
printController.present(animated: true) { _, _, error in
if let error = error {
print(error.localizedDescription)
}
}
}
}
class PrintPageRenderer: UIPrintPageRenderer, NSLayoutManagerDelegate {
var pageSize: CGSize!
var textStorage: NSTextStorage!
private let layoutManager = NSLayoutManager()
private var textViews = [UITextView]()
override var numberOfPages: Int {
if !Thread.isMainThread {
return DispatchQueue.main.sync { [self] in
numberOfPages
}
}
printFormatters = nil
layoutManager.delegate = self
textStorage.addLayoutManager(layoutManager)
if textStorage.length > 0 {
let glyphRange = layoutManager.glyphRange(forCharacterRange: NSRange(location: textStorage.length - 1, length: 0), actualCharacterRange: nil)
layoutManager.textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil)
}
var page = 0
for textView in textViews {
let printFormatter = textView.viewPrintFormatter()
addPrintFormatter(printFormatter, startingAtPageAt: page)
page += printFormatter.pageCount
}
return page
}
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
if textContainer == nil {
addPage()
}
}
private func addPage() {
let textContainer = NSTextContainer(size: pageSize)
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: CGRect(origin: .zero, size: pageSize), textContainer: textContainer)
textViews.append(textView)
}
}
I'm building a game with a client-server architecture. Using GKMatch.chooseBestHostingPlayer(_:) rarely works. When I started testing it today, it worked once at the very beginning, and since then it always succeeds on one client and returns nil on the other client. I'm testing with a Mac and an iPhone. Sometimes it fails on the Mac, sometimes on the iPhone. On the device that it succeeds on, the provided host can be the device itself or the other one.
I created FB9583628 in August 2021, but after the Feedback Assistant team replied that they are not able to reproduce it, the feedback never went forward.
import SceneKit
import GameKit
#if os(macOS)
typealias ViewController = NSViewController
#else
typealias ViewController = UIViewController
#endif
class GameViewController: ViewController, GKMatchmakerViewControllerDelegate, GKMatchDelegate {
var match: GKMatch?
var matchStarted = false
override func viewDidLoad() {
super.viewDidLoad()
GKLocalPlayer.local.authenticateHandler = authenticate
}
private func authenticate(_ viewController: ViewController?, _ error: Error?) {
#if os(macOS)
if let viewController = viewController {
presentAsSheet(viewController)
} else if let error = error {
print(error)
} else {
print("authenticated as \(GKLocalPlayer.local.gamePlayerID)")
let viewController = GKMatchmakerViewController(matchRequest: defaultMatchRequest())!
viewController.matchmakerDelegate = self
GKDialogController.shared().present(viewController)
}
#else
if let viewController = viewController {
present(viewController, animated: true)
} else if let error = error {
print(error)
} else {
print("authenticated as \(GKLocalPlayer.local.gamePlayerID)")
let viewController = GKMatchmakerViewController(matchRequest: defaultMatchRequest())!
viewController.matchmakerDelegate = self
present(viewController, animated: true)
}
#endif
}
private func defaultMatchRequest() -> GKMatchRequest {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.defaultNumberOfPlayers = 2
request.inviteMessage = "Ciao!"
return request
}
func matchmakerViewControllerWasCancelled(_ viewController: GKMatchmakerViewController) {
print("cancelled")
}
func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFailWithError error: Error) {
print(error)
}
func matchmakerViewController(_ viewController: GKMatchmakerViewController, didFind match: GKMatch) {
self.match = match
match.delegate = self
startMatch()
}
func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) {
print("\(player.gamePlayerID) changed state to \(String(describing: state))")
startMatch()
}
func startMatch() {
let match = match!
if matchStarted || match.expectedPlayerCount > 0 {
return
}
print("starting match with local player \(GKLocalPlayer.local.gamePlayerID) and remote players \(match.players.map({ $0.gamePlayerID }))")
match.chooseBestHostingPlayer { host in
print("host is \(String(describing: host?.gamePlayerID))")
}
}
}
In my SceneKit game I'm able to connect two players with GKMatchmakerViewController. Now I want to support the scenario where one of them disconnects and wants to reconnect. I tried to do this with this code:
nonisolated public func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) {
Task { @MainActor in
switch state {
case .connected:
break
case .disconnected, .unknown:
let matchRequest = GKMatchRequest()
matchRequest.recipients = [player]
do {
try await GKMatchmaker.shared().addPlayers(to: match, matchRequest: matchRequest)
} catch {
}
@unknown default:
break
}
}
}
nonisolated public func player(_ player: GKPlayer, didAccept invite: GKInvite) {
guard let viewController = GKMatchmakerViewController(invite: invite) else {
return
}
viewController.matchmakerDelegate = self
present(viewController)
}
But after presenting the view controller with GKMatchmakerViewController(invite:), nothing else happens. I would expect matchmakerViewController(_:didFind:) to be called, or how would I get an instance of GKMatch?
Here is the code I use to reproduce the issue, and below the reproduction steps.
Code
Run the attached project on an iPad and a Mac simultaneously.
On both devices, tap the ship to connect to GameCenter.
Create an automatched match by tapping the rightmost icon on both devices.
When the two devices are matched, on iPad close the dialog and tap on the ship to disconnect from GameCenter.
Wait some time until the Mac detects the disconnect and automatically sends an invitation to join again.
When the notification arrives on the iPad, tap it, then tap the ship to connect to GameCenter again. The iPad receives the call player(_:didAccept:), but nothing else, so there’s no way to get a GKMatch instance again.