Subdivide a text view's attributed string into (referable) sections
Asked Answered
Q

3

11

I'm trying to implement a very simple text editor that should work both with NSTextView on macOS and UITextView on iOS. This text editor has a toolbar button "Section Break" that inserts a new section at the current cursor position every time it is clicked. A section should be:

  1. Visually identifiable as a section (by adding visual separators between two subsequent sections and possibly adding some vertical whitespace),

  2. Referable. The user should be able to see a list of all sections in the editor and by clicking an item in that list, the text view should immediately scroll to the beginning of that section.

In another question I asked how to solve the first problem and unfortunately, I haven't found an answer on that part yet (even though there is a solution that works on macOS only).

However, this question focuses on the second aspect:
How can I maintain a list of all sections in my text view where each section keeps an accurate reference to the related text?

The complexity about this task is that the user can copy & paste or simply edit any part of the text at any time. Thus, I cannot keep a simple array of paragraph numbers or something like that.

Screenshot of several sections in a text view


What I've tried and why this appears to be such a difficult task:

  1. One idea I've had was to subclass NSTextStorage and use an array of mutable attributed strings as its internal storage. I would then use a special subclass of NSTextAttachment and use it as a section break indicator inside my text storage. The problem with that is that the text view only calls the following methods whenever the user edits text:

    func replaceCharacters(in range: NSRange, 
                           with str: String)
    

    and

    func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, 
                         range: NSRange)
    

    The first method is only passed the plain string without any attributes, the second method only gets the attributes. This means that in the first method, I cannot tell if the replacement character is actually supposed to be a section break or not, so I cannot decide at that point where to create a new element in my internal text storage array.

    In the second method, I don't know whether the user actually added new text (in which case I would have to add a new element in my text storage array for each section break attribute) or if the user simply changed some attributes of existing text (in which case the new array elements have already been created previously).

  2. I've also considered the idea of using multiple text views inside a table view or stack view. However, that doesn't work because it would keep the user from selecting (and deleting) text across multiple subsequent sections.

  3. Finally, I've tried subclassing NSLayoutManager, but the documentation on that is really thin and it seems to be the wrong place for me. (After all, the layout manager's responsibility is the layout of the text, not to keep track of its structure.)

Any ideas?

Quoit answered 20/3, 2019 at 14:35 Comment(0)
C
2

How can I maintain a list of all sections in my text view where each section keeps an accurate reference to the related text?

Start by deciding what a section is. Intuitively, it seems like a block of contiguous text, several of which are strung together to form a complete document. But try to go more deeply than that: Does all the text in a section have the same style? The same margins? Do you really need to keep track of sections at all, or would it work as well to just keep track of section breaks?

Next, since you've already decided to buy into TextKit by using NSTextView and UITextView, and since you have needs that go beyond the most common "a view with some editable text in it" use case for those classes, you need to learn more about how the underlying classes fit together. The main classes are NSTextStorage, NSTextContainer, NSLayoutManager, and of course NSTextView (or UITextView).

Understanding how the TextKit classes work and what facilities they provide to you will help you figure out how to implement your "section" concept. Here are a couple examples of ways you could go:

  • Build your own text view. Much of the work that a text view does is really done by the other classes: text container, layout manager, and text storage. You could build your own view that contains a series of text containers, so that each section is represented by a different container, each with its own layout manager and storage objects. This is obviously the most work, but it would afford a lot of control over how sections are displayed. Also, if you build your view to work on both platforms, you won't have to worry about differences between UITextView and NSTextView.

  • Let the text view do all the work. A lightweight implementation of sections could involve setting up a delegate for either the text view or text storage object. Both of those call delegate methods when editing occurs, so if your delegate maintains a list of indices or ranges that represent sections, it can use those methods to update its section boundary data. You'll still have to figure out how to show the section boundaries visually, but that could be as simple as inserting an image at each section boundary.

Coupe answered 29/3, 2019 at 13:31 Comment(4)
Thanks for the suggestions. I have read through the documentation of TextKit, the Text Programming Guide, the Cocoa Text Architecture Guide, various tutorials and watched WWDC videos. Unfortunately, the information provided is thin on the details. There are some nice abstract drawings of how the different components interact, but nothing specific.Quoit
For example, I’m having a hard time figuring out how to implement my own text view because every example I can find uses a default NS/UITextView. The NSTextStorage class even has a property textView of that particular type. That's the only way I know of how to plug all the TextKit classes together.Quoit
Apple describes how to manually create a text storage and a layout manager, they also have some notes on creating a custom text view but that is not enough for me to understand how exactly these components interact with each other.Quoit
Do you happen to know of any sample implementation of a custom text view?Quoit
R
2

Assuming that you’re using a NSTextAttachment subclass it should be fairly easy to accomplish both task, but let’s focus on the second:

Your first solution of subclassing NSTextStorage is good, but it expose the storage to a lot of unwanted logic, my suggestion is just to have your (NS|UI)TextViewDelegate to implement the func textDidChange(_ notification: Notification) and when the text change you can use the text storage to enumerate your attachments and save a list of ranges to reference later:

func textDidChange(_ notification: Notification)
{
    self.textView.textStorage?.enumerateAttribute(NSAttributedString.Key.attachment, in: NSMakeRange(0, self.textView.textStorage!.length), options: [], using: {
        (value:Any?, range:NSRange, stop:UnsafeMutablePointer<ObjCBool>) in
        // If the value is one of your attachment save the range in a list
    })
}

This way it doesn’t matter what the user is doing (writing, copy/pasting, etc…) you will always have a list of valid ranges containing your separator :)

This is just an example, your logic for re-building the list can be more refined, but I hope this can get you started.

Renoir answered 29/3, 2019 at 13:45 Comment(0)
T
-1

I would have played with the NSRange. It might not be relevant to your approach but, could be used as suggestion.

Referring to your main points.

Visually identifiable as a section (by adding visual separators between two subsequent sections and possibly adding some vertical whitespace) Referable. The user should be able to see a list of all sections in the editor and by clicking an item in that list, the text view should immediately scroll to the beginning of that section.

UIButtons could be added as sections inside the textView. The width of the button should be the width of your line inside the textView.

Pros:

  • Views or buttons inside textView are traceable and its y axis could be updated if text is pasted inside any section.
  • On selection of the button, the content offset of textView can be traced and section could be scrolled to target location.
  • Range between two buttons will give you the range of relevant text inside section. Similarly, array of sections text.
Topdrawer answered 24/3, 2019 at 18:18 Comment(2)
If you have a suggestion that does not answer the question, it would be a better idea to add it as a comment rather than an answer. It’s also not clear to me what exactly you mean by „I would have played with NSRange”.Quoit
I also cannot follow your line of thoughts with the button approach. Could you please elaborate on how exactly you would implement that (ideally with a code example) and how it would solve the problem on having a list of clickable sections?Quoit

© 2022 - 2024 — McMap. All rights reserved.