The following code only ever causes shouldDrawInsertionPoint to be printed (no drawInsertionPoint), but even if that method returns false, the blinking insertion point is still drawn. On the other hand, with TextKit 1 it works as expected.
Is there a way to hide the default insertion point in TextKit 2? My app draws its own.
I've filed FB13684251.
class TextView: NSTextView {
override var shouldDrawInsertionPoint: Bool {
print("shouldDrawInsertionPoint")
return false
}
override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
print("drawInsertionPoint", flag)
}
}
``
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Created
I'm trying to update my app to use TextKit 2. The one thing that I'm still not sure about is how I can get the selection frame. My app uses it to auto-scroll the text to keep the cursor at the same height when the text wraps onto a new line or a newline is manually inserted. Currently I'm using NSLayoutManager.layoutManager!.boundingRect(forGlyphRange:in:).
The code below almost works. When editing the text or changing the selection, the current selection frame is printed out. My expectation is that the selection frame after a text or selection change should be equal to the selection frame before the next text change. I've noticed that this is not always true when the text has a NSParagraphStyle with spacing > 0. As long as I type at the end of the text, everything's fine, but if I insert some lines, then move the selection somewhere into the middle of the text and insert another newline, the frame printed after manually moving the selection is different than the frame before the newline is inserted. It seems that the offset between the two frames is exactly the same as the paragraph style's spacing. Instead when moving the selection with the arrow key the printed frames are correct.
I've filed FB17104954.
class ViewController: NSViewController, NSTextViewDelegate {
private var textView: NSTextView!
override func loadView() {
let scrollView = NSScrollView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
textView = NSTextView(frame: scrollView.frame)
textView.autoresizingMask = [.width, .height]
textView.delegate = self
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 40
textView.typingAttributes = [.foregroundColor: NSColor.labelColor, .paragraphStyle: paragraphStyle]
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
view = scrollView
}
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
print("before", selectionFrame.maxY, selectionFrame)
return true
}
func textDidChange(_ notification: Notification) {
print("after ", selectionFrame.maxY, selectionFrame)
}
func textViewDidChangeSelection(_ notification: Notification) {
print("select", selectionFrame.maxY, selectionFrame)
}
var selectionFrame: CGRect {
guard let selection = textView.textLayoutManager!.textSelections.first?.textRanges.first else {
return .null
}
var frame = CGRect.null
textView.textLayoutManager!.ensureLayout(for: selection)
textView.textLayoutManager!.enumerateTextSegments(in: selection, type: .selection, options: [.rangeNotRequired]) { _, rect, _, _ in
frame = rect
return false
}
return frame
}
}
Apparently when setting a window to hide its title, the toolbar's displayMode is not restored when relaunching the app. For example, by default my app sets to show toolbar icons only, but when right-clicking it, selecting "Icon and Text" and relaunching the app, it's again "Icon Only".
Is there a workaround? I've filed FB17144212.
class ViewController: NSViewController, NSToolbarDelegate {
override func viewDidAppear() {
let toolbar = NSToolbar(identifier: "toolbar")
toolbar.delegate = self
toolbar.autosavesConfiguration = true
toolbar.displayMode = .iconOnly
view.window?.titleVisibility = .hidden
view.window?.toolbar = toolbar
view.window?.toolbarStyle = .unified
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [.init(rawValue: "item")]
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [.init(rawValue: "item")]
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.image = NSImage(named: NSImage.addTemplateName)!
item.label = "item"
return item
}
}
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 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?
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
I had noticed an unsettling behaviour about NSDocument some years ago and created FB7392851, but the feedback didn't go forward, so I just updated it and hopefully here or there someone can explain what's going on.
When running a simple document-based app with a text view, what I type before closing the app may be discarded without notice. To reproduce it, you can use the code below, then:
Type "asdf" in the text view.
Wait until the Xcode console logs "saving". You can trigger it by switching to another app and back again.
Type something else in the text view, such as "asdf" on a new line.
Quit the app.
Relaunch the app. The second line has been discarded.
Am I doing something wrong or is this a bug? Is there a workaround?
class ViewController: NSViewController {
@IBOutlet var textView: NSTextView!
}
class Document: NSDocument {
private(set) var text = ""
override class var autosavesInPlace: Bool {
return true
}
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
(windowController.contentViewController as? ViewController)?.textView.string = text
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
Swift.print("saving")
text = (windowControllers.first?.contentViewController as? ViewController)?.textView.string ?? ""
return Data(text.utf8)
}
override func read(from data: Data, ofType typeName: String) throws {
text = String(decoding: data, as: UTF8.self)
(windowControllers.first?.contentViewController as? ViewController)?.textView.string = text
}
}
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?
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)
}
The online documentation for fs_snapshot_create, which is on a website which apparently I'm not allowed to link to on this forum, mentions that some entitlement is necessary, but doesn't specify which one. Searching online I found someone mentioning com.apple.developer.vfs.snapshot, but when adding this to my entitlement file and building my Xcode project, I get the error
Provisioning profile "Mac Team Provisioning Profile: com.example.myApp" doesn't include the com.apple.developer.vfs.snapshot entitlement.
Searching some more online, I found someone mentioning that one has to request this entitlement from DTS. Is this true? I couldn't find any official documentation.
I actually want to make a snapshot of a user-selected directory so that my app can sync it to another volume while avoiding that the user makes changes during the sync process that would make the copy inconsistent. Would fs_snapshot_create be faster than traversing the chosen directory and creating clones of each nested file with filecopy and the flag COPYFILE_CLONE? Although I have the impression that only fs_snapshot_create could make a truly consistent snapshot.
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
}
}
When I connect to another Mac via Finder (using SMB), creating a hard link with FileManager.linkItem(atPath:toPath:) fails (both source and destination are on the remote Mac). I read online that SMB itself supports creating hard links, so is this a macOS limitation or bug?
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)
}
}
}
A user of my app reported that when trying to remove a file it always fails with the error "file couldn't be removed because you don't have permission to access it (Cocoa Error Domain 513)". After some testing, we found out that it's caused by trying to delete non-empty directories.
I'm using FileManager.removeItem(atPath:) which has worked fine for many years, but it seems that with their particular NAS, it doesn't work.
I could work around this by checking if the file is a directory, and if it is, enumerating the directory and remove each contained file before removing the directory itself. But shouldn't this already be taken care of? In the source code of FileManager I see that for Darwin platforms it calls
removefile(pathPtr, state, removefile_flags_t(REMOVEFILE_RECURSIVE))
so it seems that it should already work. Is the REMOVEFILE_RECURSIVE flag perhaps ignored by the device? But then, is the misleading "you don't have permission to access the file" error thrown by the device or by macOS?
For the FileManager source code, see https://github.com/swiftlang/swift-foundation/blob/1d5d70997410fc8b7700c8648b10d6fc28194202/Sources/FoundationEssentials/FileManager/FileOperations.swift#L444