5

I'm trying to figure out how to use NSTextList but have found little useful information online aside from this SO question and the comment in this blog.

Using this I have been able to create a crude example to figure it out:

    let textView = NSTextView()
    
    //Create NSTextList
    let list = NSTextList(markerFormat: .decimal, options: 0)
    list.startingItemNumber = 1
    
    //Create NSParagraphStyle with text list
    let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
    paragraphStyle.textLists = [list]
    
    let attributes = [NSAttributedString.Key.paragraphStyle : paragraphStyle]
    
    //Add attributes to list block
    let nsmutableAttributedString = NSMutableAttributedString(string: "\t\(list.marker(forItemNumber: 1))\tList Item 1 ", attributes: attributes)
    
    textView.textStorage?.setAttributedString(nsmutableAttributedString)
    
    return textView

This works well and when the app is run, pressing enter creates a new properly formatted line and pressing tab indents the list item correctly.

All good... However, the spacing around the marker seems to be incorrect and doesn't reflect what you would get in TextEdit for example.

Here is TextEdit, notice the spacing around the marker exhibit a

Here is my textView, notice the larger spacing around the marker which, after the procedure highlighted below, reverts to the 'default' text edit spacing...

enter image description here

There are three things I'm hoping to get out of this:

  1. Am I doing this wrong in any way? I'm pretty sure I must use \t around the marker to make it work.
  2. Any ideas on how to achieve the default spacing around the marker that apple achieves across all of their text editing apps (notes, textedit, etc)
  3. Any general tips for NSTextList. There isn't much out there so if anyone could share their knowledge that would be greatly appreciated.
santi.gs
  • 514
  • 3
  • 15

1 Answers1

0

Cautionary note: I’m also confused about this, this is my attempt at implementing the information in the comment you linked above.

I’m facing a similar issue on a project I'm working on. Discovered that the textView was changing the tabStops array (paragraphStyle.tabStops) from the default, when the user moves back from a nested list to the main one, as you’ve demonstrated in your screenshot. To verify, try printing to console the location of the first tabStop (paragraphStyle.tabStops.first.location) for the paragraph holding “List item 1” vs the paragraph holding “And the list continues”.

The naive solution that I’ve come up with, and still iterating on, is to check if the edited paragraph has:

  • A text list
  • A tabStop whose location is to the left of the default left-most tabStop location.

If the two conditions above check out, I reset the edited paragraph’s tabStops back to default.

I’m currently working on a variation of the following function specifically to resolve the issue you’ve outlined. I invoke the function inside of didProcessEditing NSTextStorage delegate method, passing in the textStorage and the editedRange.

    func handleTabStopsIssue(textStorage: NSTextStorage, editedRange: NSRange) {
        //Determine range of the edited paragraph (this is encompases the editedRange).
        let rangeOfEditedParagraph = textStorage.mutableString.paragraphRange(for: editedRange)
        //Retrieve the edited paragraph as attributed string
        let editedParagraph = textStorage.attributedSubstring(from: rangeOfEditedParagraph)
        //Enumerate the paragraphStyle attribute in the inputted paragraph NSAttributedString
        editedParagraph.enumerateAttribute(.paragraphStyle, in: NSRange(location: 0, length: editedParagraph.length), options: .longestEffectiveRangeNotRequired, using: {atts, subrange, _ in
            //Determine whether or not paragraph contains a textList
            guard let parStyle = atts as? NSParagraphStyle else { return }
            let parHasTextList = !parStyle.textLists.isEmpty
            
            //Retrieve the default tabStops to compare their location with inputted paragraph tabStop locations
            let defaultTabStops = NSParagraphStyle.default.tabStops
            guard let firstDefaultTabStop = defaultTabStops.first else { return }
            guard let inputParFirstTabStop = parStyle.tabStops.first else { return }
            print("firstDefaultTabStop \(firstDefaultTabStop) of location \(firstDefaultTabStop.location)")
            print("currentParTabStop \(inputParFirstTabStop) of location \(inputParFirstTabStop.location)")
            
            //If parStyle has textList, and its first tab stop is located to the left of the default tab stops...
            if (inputParFirstTabStop.location < firstDefaultTabStop.location) && parHasTextList {
                print(" Resetting tabstops!")
                //Set the inputted paragraph's tab stops, to default
                let newParStyle = parStyle.mutableCopy() as! NSMutableParagraphStyle
                newParStyle.tabStops = defaultTabStops
                textView.textStorage?.addAttribute(.paragraphStyle, value: newParStyle, range: rangeOfEditedParagraph)
            }
        })
    }

I've uploaded a working demo to gitHub here: https://github.com/MQumairi/TextKit-List/blob/main/List%20Problems/ViewController.swift

Prioritized clarity over efficiency, the code could be more efficient. Just wanted to show my logic.

This is still a work in progress, because the above method will not work with right-to-left languages like Arabic. Need to handle that edge case. But it is a starting point that hopefully might help others stuck on this issue.

MQumairi
  • 115
  • 2
  • 5