Nerdy internals of an Apple text editor 👨🏻‍🔧

mihhail

Mihhail Lapushkin

Posted on March 4, 2024

Nerdy internals of an Apple text editor 👨🏻‍🔧

I tried my best to convert it from the original article.

⌘+Click on the DEMO links to view the videos.


In this article, we’ll dive into the details of the way Paper functions as a TextView-based text editor for Apple platforms.

The first article was just a warm-up — here is where we get to truly geek out! 🤓

Before we start, I’ll add that for the time being Paper is built on the older TextKit 1 framework, so the article is relative to TextKit 1. That said, all of the concepts, abstractions, and principles discussed here still exist in TextKit 2, either unchanged or under a better API.

Text view

To understand how text editing works in native Apple text editors, we first need to discuss the centerpiece of the whole system — the TextView class. Technically, NSTextView and UITextView have their differences, but the API is similar enough that we can treat them as a single TextView class. I will highlight the differences where necessary.

TextView is a massive component that only grows in complexity with each release of respective operating systems. The TextEdit app consists almost entirely of a single TextView. When a single class can be used to build an entire app — you know it’s a beast.

Luckily, TextView is not just one huge pile of code. Apple tried to subdivide it into a bunch of layers — each represented by a flagship class. The layers build on top of each other to create a text editing experience.

A diagram showing the classes that make up the text view. NSTextStorage and NSTextContainer flow into NSLayoutManager which then flows into TextView. Finally, TextView flows into ScrollView. Each next class in the diagram uses the information from the previous one to, in the end, construct a complete text editor.

NSTextStorage

  • Stores the raw text string.
  • Stores the attributes (string-value pairs) assigned to ranges of text.
    • Styles such as font and color (defined by AppKit and UIKit).
    • Any string-value pair that acts as metadata for your needs.
  • Emits events about text and attribute changes.

NSTextContainer

  • Defines the shape and dimensions of the area that hosts text symbols (glyphs).
  • Most of the time it’s a rectangle (duh 🙄) but can be any shape.

NSLayoutManager

  • Figures out the dimensions of the glyphs and the spacings between them by looking at the ranges of attributes applied to the text string in NSTextStorage.
    • Extracts vector glyphs from the font.
    • Converts each text character to one or more glyphs. Some symbols and languages need more than one.
    • Calculates the size of each glyph.
    • Calculates the distances between glyphs.
    • Calculates the distances between lines of glyphs.
  • Lays out each glyph, line by line, into the shape defined by NSTextContainer.
    • Calculates where every line of text starts and ends.
    • Calculates how many lines there are and what is the total height of the text.

TextView

  • Draws the glyph layout generated by NSLayoutManager.
  • Syncs the height of the view with the current height of laid-out text.
  • Manages text input.
  • Manages the text selection.
  • Manages the caret — empty text selection.
  • Manages the typing attributes — attributes applied to the newly inserted text.
  • Can define margins (textContainerInset) around the NSTextContainer.
  • Manages all the additional bells and whistles such as dictation, copy-paste, spell check, etc.

ScrollView

  • Shows the visible portion of the TextView.
  • Manages scrolling, scroll bars, and zooming.
  • Can define its own margins (contentInset) in addition to the - textContainerInset defined by the TextView.
  • Implementation details:
    • AppKit
      • NSScrollView contains NSClipView and two instances of NSScroller.
      • NSClipView contains NSTextView.
      • Thus many separate classes work together to make the scrolling effect.
    • UIKit
      • UITextView extends from UIScrollView.
      • Thus UITextView holds everything, including the scrolling logic.
      • Another notable detail is that moving the caret outside the visible area of UITextView, bounded by contentInset, causes UITextView to auto-scroll to ensure that the caret stays within the visible area. You can often experience this in iOS text editors, where if the caret moves behind the keyboard, the editor scrolls to the next line. This is because the bottom contentInset is dynamically set to the current height of the keyboard.

A diagram breaking down the interface of the Mac app. Areas of the interface are outlined with different colors to show what classes are responsible for them.

A diagram breaking down the interface of the iPad app. Areas of the interface are outlined with different colors to show what classes are responsible for them.

A diagram breaking down the interface of the iPhone app. Areas of the interface are outlined with different colors to show what classes are responsible for them.

Attributes

With the general structure of TextView out of the way, let’s zoom in on NSTextStorage, or rather its parent class NSAttributedString, as it is the foundation of rich text editing in Apple’s frameworks.

NSAttributedString consists of two parts:

  • A regular text string.
  • String-value pairs of attributes attached to ranges of text within the string.

Attributes are used mostly for styling purposes, but nothing restricts you from assigning custom string-value pairs for your own needs.

To get started, let’s make an NSAttributedString via the API:

NSMutableAttributedString *string = [NSMutableAttributedString.alloc
  initWithString:@"The quick brown fox jumps over the lazy dog."];

NSMutableParagraphStyle *style = NSMutableParagraphStyle.new;
style.firstLineHeadIndent = 30.0;

[string addAttribute:NSParagraphStyleAttributeName
               value:style
               range:NSMakeRange(0, string.length)];
[string addAttribute:NSFontAttributeName
               value:[NSFont systemFontOfSize:25.0]
               range:NSMakeRange(0, string.length)];

[string addAttribute:NSForegroundColorAttributeName
               value:NSColor.brownColor
               range:NSMakeRange(10, 5)];

[string addAttribute:NSFontAttributeName
               value:[NSFont boldSystemFontOfSize:25.0]
               range:NSMakeRange(20, 5)];

[string addAttribute:NSBackgroundColorAttributeName
               value:NSColor.lightGrayColor
               range:NSMakeRange(26, 4)];

[string addAttribute:NSUnderlineStyleAttributeName
               value:@(NSUnderlineStyleSingle)
               range:NSMakeRange(35, 4)];
[string addAttribute:NSFontAttributeName
               value:[NSFontManager.sharedFontManager
                      convertFont:[NSFont boldSystemFontOfSize:25.0]
                      toHaveTrait:NSFontItalicTrait]
               range:NSMakeRange(35, 4)];
Enter fullscreen mode Exit fullscreen mode

NSRange is a structure consisting of a location and a length. NSMakeRange(10,5) means a range of 5 characters starting from position 10, or in other words, an inclusive range between positions 10 and 14. In case different ranges define the same attribute under the same position then the last applied range takes precedence. In the example above, the bold and italic fonts overwrite the default font that is applied to the whole string.

This code can be easily visualized in TextEdit as it is pretty much an NSTextView with some buttons.

TextEdit app window with the text “The quick brown fox jumps over the lazy dog.” The text is styled with different fonts, colors, and background colors. Every style is labeled with the names of attributes that are applied to it.

The second big part of the API is dedicated to checking what attributes are applied to what ranges. The API itself is quite peculiar. A lot of thought has gone into making it fast and efficient, but as a result, the usage can be a bit of a pain.

For instance, if you want to check whether a certain attribute exists at a certain position you would use this method:

id value = [string attribute:NSFontAttributeName
                     atIndex:6
              effectiveRange:nil];
Enter fullscreen mode Exit fullscreen mode

If the value is nil, then it does not exist. Otherwise, it is the value of the attribute which in this case is a NSFont/UIFont object. So this method can be used both to query the value and to check the existence of the attribute.

But it gets better. You can pass a pointer to the NSRange structure as the last argument (the good old C technique to return multiple values from a single function call):

NSRange effectiveRange;
id value = [string attribute:NSFontAttributeName
                     atIndex:6
              effectiveRange:&effectiveRange];
Enter fullscreen mode Exit fullscreen mode

And it will return either:

  • The range of the continuous span of the same attribute with the same value.
  • Or the range of the gap where the attribute is absent.

Two diagrams with the text “The quick brown”. The word “brown” is in brown color. In the first diagram, the NSFontAttributeName attribute is sampled at index 6. The result is “nil” and the effective range is between indexes 0 and 9 inclusive. In the second diagram, the NSFontAttributeName attribute is sampled at index 11. The result is an NSFont.brownColor object and the effective range is between indexes 10 and 14 inclusive.

Though not exactly… You see the effectiveRange here is not what you think it is. Quoting the documentation:

The range isn’t necessarily the maximum range covered by the attribute, and its extent is implementation-dependent.

In other words, it could be the correct maximum range… but it also might not be.

Ahh — I just love having a bit of non-determinism in my code!

Same diagram as the second diagram in the previous image except multiple different ranges are labeled. Labels read: “Can be this”, “Or this” or “Or even this”.

To get the guaranteed maximum range you need to use a different method.

NSRange effectiveRange;
id value = [string attribute:NSFontAttributeName
                     atIndex:6
       longestEffectiveRange:&effectiveRange
                     inRange:NSMakeRange(0, string.length)];
Enter fullscreen mode Exit fullscreen mode

I suppose, this separation is done to make the checking of the attribute existence faster with the former method as the latter one probably needs to do some range merging to figure out the longest range when multiple ranges overlap. Still — how the effectiveRange in the former method is even useful? 🤷🏼‍♂️

The same pair of methods exist to query an NSDictionary of all the attributes at a position and the effectiveRange for which this unique combination of attributes spans.

NSRange effectiveRange;
NSDictionary<NSAttributedStringKey, id> attributes =

  [string attributesAtIndex:6
      longestEffectiveRange:&effectiveRange
                    inRange:NSMakeRange(0, string.length)];
Enter fullscreen mode Exit fullscreen mode

Finally, there is a convenience method to iterate over attributes within a range. With the longest constant name that ever existed for specifying which mode of attribute range inspection you prefer.

[string enumerateAttribute:NSFontAttributeName
                   inRange:NSMakeRange(0, string.length)
  options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
  usingBlock:^(id value, NSRange range, BOOL *stop) {
  // do something
}];
Enter fullscreen mode Exit fullscreen mode

Styling

With the foundational knowledge behind us, it’s time to discuss how the syntax highlighting and text styling work in Paper.

As mentioned before, styling means applying special framework-defined attributes to ranges of text. In addition to them, Paper also uses custom attributes to identify the structure of the text before styling it. Here’s the breakdown:

  1. Meta attributes
    • Defined by the Markdown parser to identify individual parts of the Markdown syntax.
    • These are custom string-value pairs used purely for semantics.
    • They do not influence the visual look of the text.
    • You can think of them as a simplified AST for Markdown.
  2. Styling attributes
    • The visual attributes applied on top of the parts marked by meta attributes.
    • These are built-in string-value pairs defined by AppKit and UIKit.

The Mac app with the text “The quick brown fox **jumps** ==over== the ~_lazy_~ dog.” in the center. Meta and styling attributes are labeled.

The attributes are kept in sync with:

  • The Markdown text in NSTextStorage that changes due to user input.
  • The text-affecting settings that change as the user adjusts them from various menu items, sliders, and gestures.

Technically, we can identify three types of events that trigger this attribute update process:

  1. Document opened — full update of meta attributes and styling attributes.
  2. Text changed — partial update of meta attributes and styling attributes in the affected part. Most of the time only in the edited text. Sometimes in the whole paragraph. More on that in the next chapter.
  3. Setting changed — full update of styling attributes but not meta attributes.

A diagram showing the three events and how much they update the meta and styling attributes.

In every update there is a well-defined sequence of steps:

  1. Start the text editing transaction
    • Without a transaction, every attribute change would trigger an expensive layout recalc by the NSLayoutManager. Instead, we want to batch all the changes and re-layout only once in step 4.
  2. Parse the Markdown structure
    • This is where the Markdown string is broken down into pieces denoted by the meta attributes.
    • This step is skipped for setting change since the Markdown structure does not change in this case.
  3. Update layout-affecting attributes
    • The first batch of styling attributes.
    • This is every visual attribute that can influence the position or size of the glyphs in the text view.
  4. End the text editing transaction
  5. Update decorative attributes
    • The second batch of styling attributes.
    • The decorative attributes (or rendering attributes in Apple’s terminology) are applied outside the transaction. The reason is simple — they don’t affect the layout, so updating them is not expensive. And they are not even aware of the transaction since they live in the NSLayoutManager itself, not in NSTextStorage.

A diagram showing the five steps of the attribute update process on the text “**jumps** ==over”. During step 3 the font attribute is applied to “jumps”. During step 5 the light gray color is applied to Markdown tags and the background color to “over”.

The most important attribute of the layout-affecting ones is NSParagraphStyle. It defines the bulk of the values that influence the layout of the lines and paragraphs.

A diagram breaking down which parts of the text and in which way are affected by the NSParagraphStyle attribute in the Mac app.

The last chunk of attributes that participate in the styling process are the typing attributes. They are tied to the attributes at the position preceding the caret (for empty selection) or to the one at the start of the selection (for non-empty selection). Once you type a character, the typing attributes are assigned to the newly inserted text automatically. In a Markdown editor, they are not that important as the styling is derived entirely from the Markdown syntax, but they are crucial for rich text editors where the styles stick to the caret until you turn them off or move the caret to a new location. Despite being a Markdown editor, Paper does have a rich text editing experience called the Preview Mode. In this mode, the editor behaves just like a rich text editor with toggleable typing attributes being highlighted, for example, on the toolbar in the iOS app.

DEMO

Performance

The separation of meta, layout, and decorative attributes plays nicely into keeping certain editor changes fast. For instance, toggling between light and dark modes requires updating only decorative attributes which is very fast as it does not trigger the layout. Setting changes such as text size adjustments, though require a re-layout of the whole document, is still reasonably fast compared to doing that plus a full re-parse of the Markdown structure.

That said, the most crucial performance piece of any text editor is undoubtedly the typing speed. The bad news is that due to how Markdown works, any text change has the potential to affect the styling of the whole paragraph.

DEMO

Thus the logical thing to do is to re-parse and re-style the whole paragraph on every keystroke. The problem with that is while this is technically the most correct approach, it can slow down the editing for longer paragraphs. At the same time, if you’re simply typing out a long sentence, the Markdown structure does not change. There is really no need to re-style everything all the time for those simple typing scenarios.

So to make typing snappier, I’ve built an algorithm that looks at the next character being typed as well as what characters are around it. The gist of the logic is that if you’re typing a special Markdown symbol, or the location of the edit is surrounded by one, then you should update the whole paragraph, otherwise you can simply rely on the typing attributes. It’s a simple algorithm that does marvels for the speed of the editor in the majority of typing situations.

Two diagrams. In the first diagram the letter “p” is inserted into the text “**jums** ==over=” between “m” and “s”. The newly inserted letter “p” is restyled as a result. In the second diagram the letter “*” is deleted from “**jumps** ==over”. The whole paragraph is restyled as a result and “jumps” is no longer bold.

The only nasty exception to the above is when you have code blocks in the document. Code blocks are the only multi-paragraph Markdown constructs in Paper. A keystroke has the potential to re-style the whole document.

DEMO

For now, I decided to ignore code blocks in documents beyond a certain character limit. It keeps the editor fast for the majority of users who don’t care about code, at the same time making Paper more useful for dev-adjacent audiences.

The final technique that I use to speed things up is to cache every complex value object in the string-value attribute pair.

  • NSFont/UIFont
  • NSColor/UIColor
  • NSParagraphStyle

They are being re-assigned on every keystroke and never change unless a text-affecting setting is changed, so it makes sense to reuse them instead of creating new instances every time.

Meta attributes

Besides the highlighting logic, meta attributes play a crucial role in various features that need to know about the structure of the text.

Formatting shortcuts

  • Toggling styles on a selected piece of Markdown text requires detailed information about the existing Markdown styles inside the selection.
  • If the selection completely encloses the same style, then the style is removed.
  • If the selection does not contain the same style, then the style is added.
  • If the selection partially encloses the same style, then the style is moved to the selection.
  • You also need to be careful not to mix the styles that cannot be mixed. The conflicting styles need to be removed first, before a new style can be added. For example, styles that define the type of the paragraph such as heading and blockquote cannot be mixed.

DEMO

Jumping between chapters

  • Paper has a feature that allows you to jump to the previous or the next edge of the chapter.
  • Meta attributes help to locate the headings relative to the position of the caret.

DEMO

Outline

  • The outline feature relies on being able to traverse every heading.
  • Pressing on the item in the outline moves the caret to that chapter.

DEMO

Rearranging chapters

  • Paper also has a feature that allows rearranging chapters in the outline.

DEMO

Converting formats

  • Converting the Markdown content to RTF, HTML, and DOCX relies on knowing the structure of the text.
  • Since Paper does not include any external libraries, having a pre-parsed model of the text allows me to traverse the structure, building the respective output format in the process.
- (NSString *)toHtml:(NSMutableAttributedString *)string {
  [self encloseInHtmlTags:string
                         :MdStrongAttributeName
                         :@{
    MdStrong: @[ @"<strong>", @"</strong>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdEmphasisAttributeName
                         :@{
    MdEmphasis: @[ @"<em>", @"</em>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdUnderlineAttributeName
                         :@{
    MdUnderline: @[ @"<u>", @"</u>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdStrikethroughAttributeName
                         :@{
    MdStrikethrough: @[ @"<s>", @"</s>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdHighlightAttributeName
                         :@{
    MdHighlight: @[ @"<mark>", @"</mark>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdCodeAttributeName
                         :@{
    MdCode: @[ @"<code>", @"</code>" ]
  }];
  [self encloseInHtmlTags:string
                         :MdHeadingAttributeName
                         :@{
    MdHeading1: @[ @"<h1>", @"</h1>" ],
    MdHeading2: @[ @"<h2>", @"</h2>" ],
    MdHeading3: @[ @"<h3>", @"</h3>" ],
    MdHeading4: @[ @"<h4>", @"</h4>" ],
    MdHeading5: @[ @"<h5>", @"</h5>" ],
    MdHeading6: @[ @"<h6>", @"</h6>" ]
  }];
  [self encloseInHtmlTags:string
                         :ParagraphAttributeName
                         :@{
    Paragraph: @[ @"<p>", @"</p>" ]
  }];

  [self encloseInBlockquoteHtmlTags:string];
  [self encloseInListHtmlTags:string];
  [self transformFootnotesForHtml:string];
  [self deleteCharactersWithAttributes:string :MetaAttributes.tags];
  [self insertHtmlBreaksOnEmptyLines:string];

  return string;
}
Enter fullscreen mode Exit fullscreen mode

Text container math

The most important rule for the text container is to maintain the preferred line length, dividing the remaining space between side insets.

A diagram breaking down the interface of the Mac app. The text container is centered and the side margins are of the same width.

There are however trickier cases where you need to fake the symmetry. Like when the heading tags are placed outside of the regular flow of text. The text container is shifted to the left and the paragraphs are indented with NSParagraphStyle.

A diagram breaking down the interface of the Mac app. Various gaps and dimensions are labeled.

While there is enough space, it tries to keep the margins visually symmetrical. If there is no extra space left, then it breaks the symmetry in favor of keeping the specified line length. But only while there is padding remaining on the right side. When there is no padding left, the minimum margins take precedence over keeping the line length to its preferred width.

DEMO

You can achieve this gradual collapsing with a combination of min and max functions. It takes a second or two to get your head around the math, but once you do, it feels quite elegant in my opinion. I love this kind of simple mathy code that leads to beautiful visual results.

- (CGFloat)leftInset {
  return (self.availableInsetWidth - fmin(
    self.availableInsetWidth - self.totalMinInset,
    self.leftPadding
  )) / 2.0;
}

- (CGFloat)rightInset {
  return self.availableInsetWidth - self.leftInset;
}

- (CGFloat)availableInsetWidth {
  return self.availableWidth - self.textContainerWidth;
}

- (CGFloat)textContainerWidth {
  return fmin(
    self.maxContentWidth,
    self.availableWidth - self.totalMinInset
  );
}

- (CGFloat)maxContentWidth {
  return self.lineLength * self.characterWidth + self.leftPadding;
}

- (CGFloat)availableWidth {
  return CGRectGetWidth(self.clipView.bounds);
}

- (CGFloat)totalMinInset {
  return self.minInset * 2.0;
}

- (CGFloat)minInset {
  return
    CGRectGetMinX(self.window.titlebarButtonGroupBoundingRect_) +
    CGRectGetMaxX(self.window.titlebarButtonGroupBoundingRect_);
}

- (CGFloat)leftPadding {
  return [@"### " sizeWithAttributes:@{
    NSFontAttributeName: Font.body
  }].width;
}
Enter fullscreen mode Exit fullscreen mode

Selection anchoring

Text selection always has an anchor point. It’s something we are so used to that we never stop to think about.

On the Mac, we click and drag to select the text and we instinctively know that the selection will increase when dragging to the right and decrease when dragging to the left. But only until we hit the point of the click. Then the opposite happens.

DEMO

On iOS the selection is a bit more interactive. We can drag one edge and then the other one becomes the anchor, and vice versa.

DEMO

The same logic applies when we extend the selection with the keyboard. Hold the Option key plus a left or a right arrow and you can jump between the edges of the words. Do the same while holding the Shift key, in addition to the Option key, and you can select with word increments. And again — it remembers where you started.

It even works naturally when you first click and drag and then continue extending or shrinking the selection with the keyboard. The initial point of the click remains the anchor.

DEMO

Selection affinity

Another fascinating concept of text editing that you most probably don’t know about is selection affinity. Quoting Apple’s documentation:

Selection affinity determines whether, for instance, the insertion point appears after the last character on a line or before the first character on the following line in cases where text wraps across line boundaries.

My guess is you still have no clue what it means, so let’s see it in action.

Pay attention to the screencast below. When I move the caret with the arrow keys, it simply switches the lines when moving around the wrapping point denoted by the space character. However, if I move the caret to the end of the line with the shortcut, it attaches itself to the right side of the wrapping space while staying on the same line.

DEMO

There are also other instances where the TextView decides to play this trick. It’s a tiny detail and sort of makes sense when you think about it, but quite hard to actually notice.

Uniform Type Identifiers

The last chapter will focus on cross-app data exchange, but first, we need to discuss the system that underpins it — the UTIs. It’s a hierarchical system where data types conform to (inherit from) parent data types.

  • public.* types are defined by Apple. They identify the widely accepted formats such as public.html and public.jpeg.
  • Developers can create their own identifiers using the reverse domain naming scheme to avoid collisions.

A diagram showing the hierarchical structure of UTIs. At the top is “public.data”, below it “public.text”. Then it splits to “public.plain-text” and “public.rtf”. Below “public.plain-text” is “net.daringfireball.markdown”.

The benefit of the hierarchical system is that, for example, if your app can view any text format then you don’t need to list all of them — you can just say that it works with public.text. And indeed, Paper declares that it can open any text file, and although you won’t get any highlighting, you can still open .html, .rtf, or any other text format.

RTF file opened in the Mac app.

When exchanging data via a programmatic interface such as the clipboard, UTIs can be used directly. Files however are a bit trickier. File is a cross-platform concept and de-facto identifiers for files in the cross-platform realm are file extensions. Even if Apple would redo their systems to rely on some file-level UTI metadata field instead of the file extension (and it appears they have), other systems would not know anything about it. So to stay compatible, every UTI can define one or more file extensions that are associated with it.

Now, most of the time you work with either public UTIs or private ones that you’ve created specifically for your app. Things are relatively straightforward in these scenarios. The harder case is when you have a format that’s widely accepted, but not defined by Apple. This is exactly the case with Markdown. I will explain some of the annoying edge cases with these semi-public UTIs in the next chapter.

Pasteboard

UTIs transition nicely into the topic of cross-app exchange driven primarily by the clipboard, or in Apple’s technical terms — the pasteboard.

The pasteboard is nothing more than a dictionary where UTIs are mapped to serialized data — in either textual or binary format. In fact, using the Clipboard Viewer from Additional Tools for Xcode you can inspect the contents of the pasteboard in real time.

DEMO

As you can see, a single copy action writes multiple representations of the same data at once (for backward compatibility some apps also write legacy non-UTI identifiers such as NeXT Rich Text Format v1.0 pasteboard type). That’s how, for instance, if you copy from Pages and paste it into MarkEdit — you get just the text, but if you paste it into TextEdit — you get the whole shebang.

DEMO

As a general rule, editors pick whatever is the richest format they can handle. Some apps provide ways to force a specific format to be used. For example, a common menu item in the Edit menu of rich text editors is Paste and Match Style or Paste as Plain Text. It tells the app to use the plain text format from the pasteboard. The styles applied to the pasted text are usually taken from the typing attributes.

A fun fact is that drag and drop is also powered by the pasteboard, but a different one. The standard one is called the general pasteboard and it’s used for copy-paste. You can even create custom ones for bespoke cross-app interactions.

Another fun fact is that RTF is basically the serialized form of NSAttributedString. Or vice versa, NSAttributedString is the programmatic interface for RTF.

NSAttributedString *string = [NSAttributedString.alloc initWithString:
  @"The quick brown fox jumps over the lazy dog."];
NSData *data = [string dataFromRange:NSMakeRange(0, string.length)
                     documentAttributes:@{
  NSDocumentTypeDocumentOption: NSRTFTextDocumentType
} error:nil];

// {\rtf1\ansi\ansicpg1252\cocoartf2759…
NSLog(@"%@", [NSString.alloc initWithData:data]);
Enter fullscreen mode Exit fullscreen mode

This means that TextView is out-of-the-box compatible with the pasteboard since it works on top of NSTextStorage — the child class of NSAttributedString. No extra coding is needed to copy the contents to the pasteboard.

Now, as I mentioned in the last chapter, this is all great for public UTIs. But what about semi-public ones like Markdown? From my experience, the cross-app exchange is a mixed bag…

Imagine you want to copy from one Markdown editor and paste it into another one. Let’s say both have implemented the standard protocol to export formats with various levels of richness and to import the richest format given. Copying from the first editor exports Markdown as public.text and the rich text representation as public.rtf. When pasting to the second editor, it will pick public.rtf instead of the native Markdown format since there is no indication that the text is indeed Markdown. You end up with this weird double conversion that leads to all sorts of small formatting issues, such as extra newlines due to slight variations in the way Markdown↔RTF translation works in both apps, as well as just fundamental styling differences between Markdown and RTF. For the user it is obvious — “I copy Markdown from here and paste it here — it should just copy 1:1”, but under the hood there is a lot of needless conversion.

For this to work nicely, both apps should magically agree to export the net.daringfireball.markdown UTI and prefer it over public.rtf. If only one of the apps does it — it won’t make a difference. Paper tried to be a good citizen by exporting the Markdown UTI, but none of the other apps seem to prefer it over rich text. In addition to that, Pages has a weird behavior where it does prefer net.daringfireball.markdown over public.rtf, but in doing so it just inserts the raw Markdown string as is without converting it to rich text (why-y-y??? 😫). For this reason, I had to drop the Markdown UTI.

But why export RTF at all? Markdown is all about plain text — drop RTF and problem solved” — you might think. Well, that’s true, but I want to provide a seamless copy-paste experience from Paper to rich text editors. And being a good OS citizen, you should provide many formats that represent the copied data, so that the receiving application could pick the richest one it can handle. In Paper, you can copy the Markdown text from the editor and paste it into the Mail app, and it would paste as nicely formatted rich text, not as some variant of Markdown. This is a great experience in my opinion. The only problem is that it often leads to less-than-ideal UX in other cases.

DEMO

Another feature closely related to the pasteboard is sharing on iOS. It’s quite similar to copy-paste, only with a bit of UI on top. Your app exports data in various formats and the receiving app decides what format it wants to grab. Strangely enough, UTIs are not used to identify the data (well actually they kind of are through some bizarre scripting language in a config file 😱). Rather, classes such as NSAttributedString, NSURL, and UIImage are directly used to represent the type. Unlike the pasteboard that applies to all apps automatically, the sharing feature on iOS requires apps to explicitly opt-in to be present in that top row of apps by providing a share extension with a custom UI.

DEMO

That’s it for now

Check out the first article if you haven’t already. It has a lot more tidbits about the app and the development process.

💖 💪 🙅 🚩
mihhail
Mihhail Lapushkin

Posted on March 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related