<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mihhail Lapushkin</title>
    <description>The latest articles on Forem by Mihhail Lapushkin (@mihhail).</description>
    <link>https://forem.com/mihhail</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1252566%2F4f6d6010-c19d-4cf5-8ba9-097eb78fd22b.png</url>
      <title>Forem: Mihhail Lapushkin</title>
      <link>https://forem.com/mihhail</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mihhail"/>
    <language>en</language>
    <item>
      <title>Nerdy internals of an Apple text editor 👨🏻‍🔧</title>
      <dc:creator>Mihhail Lapushkin</dc:creator>
      <pubDate>Mon, 04 Mar 2024 13:41:35 +0000</pubDate>
      <link>https://forem.com/mihhail/nerdy-internals-of-an-apple-text-editor-10fd</link>
      <guid>https://forem.com/mihhail/nerdy-internals-of-an-apple-text-editor-10fd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I tried my best to convert it from the &lt;a href="https://papereditor.app/internals"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;⌘+Click on the &lt;strong&gt;DEMO&lt;/strong&gt; links to view the videos.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In this article, we’ll dive into the details of the way &lt;a href="https://papereditor.app"&gt;Paper&lt;/a&gt; functions as a &lt;code&gt;TextView&lt;/code&gt;-based text editor for Apple platforms.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://papereditor.app/dev"&gt;first article&lt;/a&gt; was just a warm-up — here is where we get to truly geek out! 🤓&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Text view
&lt;/h2&gt;

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

&lt;p&gt;&lt;code&gt;TextView&lt;/code&gt; is a massive component that only &lt;a href="https://papereditor.app/dev#gnarly-stuff"&gt;grows in complexity&lt;/a&gt; with each release of respective operating systems. The TextEdit app consists almost entirely of a single &lt;code&gt;TextView&lt;/code&gt;. When a single class can be used to build an entire app — you know it’s a beast.&lt;/p&gt;

&lt;p&gt;Luckily, &lt;code&gt;TextView&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aFSOhnV2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-classes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aFSOhnV2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-classes.png" alt="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." width="800" height="1709"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;NSTextStorage&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Stores&lt;/em&gt; the raw text string.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Stores&lt;/em&gt; the attributes (string-value pairs) assigned to ranges of text.

&lt;ul&gt;
&lt;li&gt;Styles such as font and color (defined by AppKit and UIKit).&lt;/li&gt;
&lt;li&gt;Any string-value pair that acts as metadata for your needs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Emits events about text and attribute changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;NSTextContainer&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Defines the shape and dimensions of the area that hosts text symbols (glyphs).&lt;/li&gt;
&lt;li&gt;Most of the time it’s a rectangle (duh 🙄) but can be any shape.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;NSLayoutManager&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;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 &lt;code&gt;NSTextStorage&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Extracts vector glyphs from the font.&lt;/li&gt;
&lt;li&gt;Converts each text character to one or more glyphs. Some symbols and languages need more than one.&lt;/li&gt;
&lt;li&gt;Calculates the size of each glyph.&lt;/li&gt;
&lt;li&gt;Calculates the distances between glyphs.&lt;/li&gt;
&lt;li&gt;Calculates the distances between lines of glyphs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Lays out each glyph, line by line, into the shape defined by &lt;code&gt;NSTextContainer&lt;/code&gt;.

&lt;ul&gt;
&lt;li&gt;Calculates where every line of text starts and ends.&lt;/li&gt;
&lt;li&gt;Calculates how many lines there are and what is the total height of the text.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;TextView&lt;/code&gt;
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  &lt;code&gt;ScrollView&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Shows the visible portion of the &lt;code&gt;TextView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Manages scrolling, scroll bars, and zooming.&lt;/li&gt;
&lt;li&gt;Can define its own margins (&lt;code&gt;contentInset&lt;/code&gt;) in addition to the - &lt;code&gt;textContainerInset&lt;/code&gt; defined by the &lt;code&gt;TextView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implementation details:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AppKit&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NSScrollView&lt;/code&gt; contains &lt;code&gt;NSClipView&lt;/code&gt; and two instances of &lt;code&gt;NSScroller&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSClipView&lt;/code&gt; contains &lt;code&gt;NSTextView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Thus many separate classes work together to make the scrolling effect.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UIKit&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UITextView&lt;/code&gt; extends from &lt;code&gt;UIScrollView&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Thus &lt;code&gt;UITextView&lt;/code&gt; holds everything, including the scrolling logic.&lt;/li&gt;
&lt;li&gt;Another notable detail is that moving the caret outside the visible area of &lt;code&gt;UITextView&lt;/code&gt;, bounded by &lt;code&gt;contentInset&lt;/code&gt;, causes &lt;code&gt;UITextView&lt;/code&gt; to &lt;em&gt;auto-scroll&lt;/em&gt; 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 &lt;em&gt;contentInset&lt;/em&gt; is dynamically set to the current height of the keyboard.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pszvTxf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-mac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pszvTxf8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-mac.png" alt="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." width="800" height="1323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--joCxpcsQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-ipad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--joCxpcsQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-ipad.png" alt="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." width="800" height="1700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Txiw6tBw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-iphone.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Txiw6tBw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-view-iphone.png" alt="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." width="800" height="1775"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Attributes
&lt;/h2&gt;

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

&lt;p&gt;&lt;code&gt;NSAttributedString&lt;/code&gt; consists of two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A regular text string.&lt;/li&gt;
&lt;li&gt;String-value pairs of attributes attached to ranges of text within the string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attributes are used mostly for styling purposes, but nothing restricts you from assigning custom string-value pairs for your own needs.&lt;/p&gt;

&lt;p&gt;To get started, let’s make an &lt;code&gt;NSAttributedString&lt;/code&gt; via the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSMutableAttributedString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSMutableAttributedString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alloc&lt;/span&gt;
  &lt;span class="nl"&gt;initWithString:&lt;/span&gt;&lt;span class="s"&gt;@"The quick brown fox jumps over the lazy dog."&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;NSMutableParagraphStyle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSMutableParagraphStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstLineHeadIndent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSParagraphStyleAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSFont&lt;/span&gt; &lt;span class="nf"&gt;systemFontOfSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSForegroundColorAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="n"&gt;NSColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brownColor&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSFont&lt;/span&gt; &lt;span class="nf"&gt;boldSystemFontOfSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSBackgroundColorAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="n"&gt;NSColor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lightGrayColor&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSUnderlineStyleAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSUnderlineStyleSingle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;addAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
               &lt;span class="nl"&gt;value:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSFontManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedFontManager&lt;/span&gt;
                      &lt;span class="nl"&gt;convertFont:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSFont&lt;/span&gt; &lt;span class="nf"&gt;boldSystemFontOfSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                      &lt;span class="nl"&gt;toHaveTrait:&lt;/span&gt;&lt;span class="nf"&gt;NSFontItalicTrait&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
               &lt;span class="nl"&gt;range:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NSRange&lt;/code&gt; is a structure consisting of a &lt;code&gt;location&lt;/code&gt; and a &lt;code&gt;length&lt;/code&gt;. &lt;code&gt;NSMakeRange(10,5)&lt;/code&gt; means a range of &lt;code&gt;5&lt;/code&gt; characters starting from position &lt;code&gt;10&lt;/code&gt;, or in other words, an inclusive range between positions &lt;code&gt;10&lt;/code&gt; and &lt;code&gt;14&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;This code can be easily visualized in TextEdit as it is pretty much an &lt;code&gt;NSTextView&lt;/code&gt; with some buttons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GdUlBVDj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-attributes-in-textedit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GdUlBVDj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-attributes-in-textedit.png" alt="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." width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For instance, if you want to check whether a certain attribute exists at a certain position you would use this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
                     &lt;span class="nl"&gt;atIndex:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
              &lt;span class="nl"&gt;effectiveRange:&lt;/span&gt;&lt;span class="nb"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;But it gets better. You can pass a pointer to the &lt;code&gt;NSRange&lt;/code&gt; structure as the last argument (the good old C technique to return multiple values from a single function call):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSRange&lt;/span&gt; &lt;span class="n"&gt;effectiveRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
                     &lt;span class="nl"&gt;atIndex:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
              &lt;span class="nl"&gt;effectiveRange:&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;effectiveRange&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it will return either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The range of the continuous span of the same attribute with the same value.&lt;/li&gt;
&lt;li&gt;Or the range of the gap where the attribute is absent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yOY6I8FO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-attributes-at-index.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yOY6I8FO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-attributes-at-index.png" alt="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." width="800" height="919"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Though not exactly… You see the &lt;code&gt;effectiveRange&lt;/code&gt; here is not what you think it is. Quoting the documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The range isn’t necessarily the maximum range covered by the attribute, and its extent is implementation-dependent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, it &lt;em&gt;could be&lt;/em&gt; the correct maximum range… but it also &lt;em&gt;might not be&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;“&lt;em&gt;Ahh — I just love having a bit of non-determinism in my code!&lt;/em&gt;”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_kNJBK6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/effective-range.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_kNJBK6a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/effective-range.png" alt="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”." width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get the guaranteed maximum range you need to use a different method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSRange&lt;/span&gt; &lt;span class="n"&gt;effectiveRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
                     &lt;span class="nl"&gt;atIndex:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
       &lt;span class="nl"&gt;longestEffectiveRange:&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;effectiveRange&lt;/span&gt;
                     &lt;span class="nl"&gt;inRange:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I suppose, this separation is done to make the &lt;em&gt;checking&lt;/em&gt; 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 &lt;code&gt;effectiveRange&lt;/code&gt; in the former method is even useful? 🤷🏼‍♂️&lt;/p&gt;

&lt;p&gt;The same pair of methods exist to query an &lt;code&gt;NSDictionary&lt;/code&gt; of all the attributes at a position and the &lt;code&gt;effectiveRange&lt;/code&gt; for which this unique combination of attributes spans.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSRange&lt;/span&gt; &lt;span class="n"&gt;effectiveRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;NSDictionary&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NSAttributedStringKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;attributesAtIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;
      &lt;span class="nl"&gt;longestEffectiveRange:&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;effectiveRange&lt;/span&gt;
                    &lt;span class="nl"&gt;inRange:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;enumerateAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSFontAttributeName&lt;/span&gt;
                   &lt;span class="nl"&gt;inRange:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nl"&gt;options:&lt;/span&gt;&lt;span class="n"&gt;NSAttributedStringEnumerationLongestEffectiveRangeNotRequired&lt;/span&gt;
  &lt;span class="nl"&gt;usingBlock:&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NSRange&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// do something&lt;/span&gt;
&lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Styling
&lt;/h2&gt;

&lt;p&gt;With the foundational knowledge behind us, it’s time to discuss how the syntax highlighting and text styling work in Paper.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Meta attributes&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Defined by the Markdown parser to identify individual parts of the Markdown syntax.&lt;/li&gt;
&lt;li&gt;These are custom string-value pairs used purely for semantics.&lt;/li&gt;
&lt;li&gt;They do not influence the visual look of the text.&lt;/li&gt;
&lt;li&gt;You can think of them as a simplified &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree"&gt;AST&lt;/a&gt; for Markdown.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling attributes&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The visual attributes applied on top of the parts marked by meta attributes.&lt;/li&gt;
&lt;li&gt;These are built-in string-value pairs defined by AppKit and UIKit.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BEM-BJYa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/meta-and-styling-attributes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BEM-BJYa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/meta-and-styling-attributes.png" alt="The Mac app with the text “The quick brown fox **jumps** ==over== the ~_lazy_~ dog.” in the center. Meta and styling attributes are labeled." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The attributes are kept in sync with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Markdown text&lt;/strong&gt; in &lt;code&gt;NSTextStorage&lt;/code&gt; that changes due to user input.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;text-affecting settings&lt;/strong&gt; that change as the user adjusts them from various menu items, sliders, and gestures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technically, we can identify three types of events that trigger this attribute update process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Document opened&lt;/strong&gt; — full update of meta attributes and styling attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text changed&lt;/strong&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting changed&lt;/strong&gt; — full update of styling attributes but not meta attributes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kLSrUGtk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/attribute-update-events.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kLSrUGtk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/attribute-update-events.png" alt="A diagram showing the three events and how much they update the meta and styling attributes." width="800" height="979"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In every update there is a well-defined sequence of steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start the text editing transaction&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Without a transaction, every attribute change would trigger an expensive layout recalc by the &lt;code&gt;NSLayoutManager&lt;/code&gt;. Instead, we want to batch all the changes and re-layout only once in step 4.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse the Markdown structure&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;This is where the Markdown string is broken down into pieces denoted by the &lt;em&gt;meta attributes&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;This step is skipped for &lt;em&gt;setting change&lt;/em&gt; since the Markdown structure does not change in this case.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update layout-affecting attributes&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The first batch of &lt;em&gt;styling attributes&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;This is every visual attribute that can influence the position or size of the glyphs in the text view.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End the text editing transaction&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update decorative attributes&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The second batch of &lt;em&gt;styling attributes&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The decorative attributes (or &lt;em&gt;rendering attributes&lt;/em&gt; 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 &lt;code&gt;NSLayoutManager&lt;/code&gt; itself, not in &lt;code&gt;NSTextStorage&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_6dp78RK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/attribute-update-steps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_6dp78RK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/attribute-update-steps.png" alt="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”." width="800" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most important attribute of the layout-affecting ones is &lt;code&gt;NSParagraphStyle&lt;/code&gt;. It defines the bulk of the values that influence the layout of the lines and paragraphs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--W0JLitH1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/paragraph-style.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--W0JLitH1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/paragraph-style.png" alt="A diagram breaking down which parts of the text and in which way are affected by the NSParagraphStyle attribute in the Mac app." width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last chunk of attributes that participate in the styling process are the &lt;em&gt;typing attributes&lt;/em&gt;. 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 &lt;em&gt;stick&lt;/em&gt; to the caret until you turn them off or move the caret to a new location. Despite being a Markdown editor, Paper &lt;em&gt;does&lt;/em&gt; have a rich text editing experience called the Preview Mode. In this mode, the editor behaves just like a rich text editor with &lt;em&gt;toggleable&lt;/em&gt; typing attributes being highlighted, for example, on the toolbar in the iOS app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/typing-attributes.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/md-affecting-paragraph.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OL0atgHA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/typing-optimization.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OL0atgHA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/typing-optimization.png" alt="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." width="800" height="902"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/md-code-blocks.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The final technique that I use to speed things up is to cache every complex value object in the string-value attribute pair.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NSFont&lt;/code&gt;/&lt;code&gt;UIFont&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSColor&lt;/code&gt;/&lt;code&gt;UIColor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NSParagraphStyle&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meta attributes
&lt;/h2&gt;

&lt;p&gt;Besides the highlighting logic, meta attributes play a crucial role in various features that need to know about the structure of the text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting shortcuts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Toggling styles on a selected piece of Markdown text requires detailed information about the existing Markdown styles inside the selection.&lt;/li&gt;
&lt;li&gt;If the selection completely encloses the same style, then the style is removed.&lt;/li&gt;
&lt;li&gt;If the selection does not contain the same style, then the style is added.&lt;/li&gt;
&lt;li&gt;If the selection partially encloses the same style, then the style is moved to the selection.&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;heading&lt;/em&gt; and &lt;em&gt;blockquote&lt;/em&gt; cannot be mixed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/formatting-shortcuts.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Jumping between chapters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Paper has a feature that allows you to jump to the previous or the next edge of the chapter.&lt;/li&gt;
&lt;li&gt;Meta attributes help to locate the headings relative to the position of the caret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/jump-chapters.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Outline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The outline feature relies on being able to traverse every heading.&lt;/li&gt;
&lt;li&gt;Pressing on the item in the outline moves the caret to that chapter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/outline.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Rearranging chapters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Paper also has a feature that allows rearranging chapters in the outline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/rearrange-chapters.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Converting formats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Converting the Markdown content to RTF, HTML, and DOCX relies on knowing the structure of the text.&lt;/li&gt;
&lt;li&gt;Since Paper &lt;a href="https://papereditor.app/dev#third-party-dependencies"&gt;does not include any external libraries&lt;/a&gt;, having a pre-parsed model of the text allows me to traverse the structure, building the respective output format in the process.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;toHtml&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSMutableAttributedString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdStrongAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdStrong:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;strong&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/strong&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdEmphasisAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdEmphasis:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;em&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/em&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdUnderlineAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdUnderline:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;u&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/u&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdStrikethroughAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdStrikethrough:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;s&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/s&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdHighlightAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdHighlight:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;mark&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/mark&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdCodeAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdCode:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;code&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/code&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MdHeadingAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading1:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h1&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading2:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h2&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h2&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading3:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h3&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h3&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading4:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h4&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h4&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading5:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h5&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h5&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;MdHeading6:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;h6&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/h6&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;
                         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ParagraphAttributeName&lt;/span&gt;
                         &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
    &lt;span class="nl"&gt;Paragraph:&lt;/span&gt; &lt;span class="p"&gt;@[&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;p&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;@"&amp;lt;/p&amp;gt;"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInBlockquoteHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;encloseInListHtmlTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;transformFootnotesForHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;deleteCharactersWithAttributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;MetaAttributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;insertHtmlBreaksOnEmptyLines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Text container math
&lt;/h2&gt;

&lt;p&gt;The most important rule for the text container is to maintain the preferred line length, dividing the remaining space between side insets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--U5coxGrV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-container-math.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--U5coxGrV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-container-math.png" alt="A diagram breaking down the interface of the Mac app. The text container is centered and the side margins are of the same width." width="800" height="831"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;NSParagraphStyle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--shK8EtSd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-container-symmetry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--shK8EtSd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/text-container-symmetry.png" alt="A diagram breaking down the interface of the Mac app. Various gaps and dimensions are labeled." width="800" height="1061"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/text-container-inset.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can achieve this gradual collapsing with a combination of &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;leftInset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availableInsetWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availableInsetWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalMinInset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leftPadding&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;rightInset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availableInsetWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leftInset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;availableInsetWidth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContainerWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;textContainerWidth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxContentWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalMinInset&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;maxContentWidth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineLength&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;characterWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leftPadding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;availableWidth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CGRectGetWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clipView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;totalMinInset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minInset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;minInset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;CGRectGetMinX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titlebarButtonGroupBoundingRect_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;CGRectGetMaxX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titlebarButtonGroupBoundingRect_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;leftPadding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;@"### "&lt;/span&gt; &lt;span class="nf"&gt;sizeWithAttributes&lt;/span&gt;&lt;span class="p"&gt;:@{&lt;/span&gt;
    &lt;span class="nl"&gt;NSFontAttributeName:&lt;/span&gt; &lt;span class="n"&gt;Font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;
  &lt;span class="p"&gt;}].&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Selection anchoring
&lt;/h2&gt;

&lt;p&gt;Text selection always has an anchor point. It’s something we are so used to that we never stop to think about.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/selection-mac.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/selection-ios.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/selection-anchoring.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Selection affinity
&lt;/h2&gt;

&lt;p&gt;Another fascinating concept of text editing that you &lt;em&gt;most probably&lt;/em&gt; don’t know about is &lt;em&gt;selection affinity&lt;/em&gt;. Quoting Apple’s documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My guess is you still have no clue what it means, so let’s see it in action.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;space&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/selection-affinity.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are also other instances where the &lt;code&gt;TextView&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uniform Type Identifiers
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;conform to&lt;/em&gt; (inherit from) parent data types.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public.*&lt;/code&gt; types are defined by Apple. They identify the widely accepted formats such as &lt;code&gt;public.html&lt;/code&gt; and &lt;code&gt;public.jpeg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Developers can create their own identifiers using the &lt;em&gt;reverse domain naming scheme&lt;/em&gt; to avoid collisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7tNbPAR2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/utis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7tNbPAR2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/utis.png" alt="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”." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;public.text&lt;/code&gt;. And indeed, Paper declares that it can open any text file, and although you won’t get any highlighting, you can still open &lt;code&gt;.html&lt;/code&gt;, &lt;code&gt;.rtf&lt;/code&gt;, or any other text format.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--igi1obpp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/rtf-in-paper.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--igi1obpp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/internals-article/rtf-in-paper.png" alt="RTF file opened in the Mac app." width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;file extensions&lt;/em&gt;. Even if Apple would redo their systems to rely on some file-level UTI metadata field instead of the file extension (and &lt;a href="https://stackoverflow.com/questions/34927527"&gt;it appears they have&lt;/a&gt;), 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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://daringfireball.net/linked/2011/08/05/markdown-uti"&gt;Markdown&lt;/a&gt;. I will explain some of the annoying edge cases with these &lt;em&gt;semi-public&lt;/em&gt; UTIs in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pasteboard
&lt;/h2&gt;

&lt;p&gt;UTIs transition nicely into the topic of cross-app exchange driven primarily by the &lt;em&gt;clipboard&lt;/em&gt;, or in Apple’s technical terms — the &lt;em&gt;pasteboard&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;Clipboard Viewer&lt;/em&gt; from &lt;em&gt;Additional Tools for Xcode&lt;/em&gt; you can inspect the contents of the pasteboard in real time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/clipboard-viewer.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, a single &lt;em&gt;copy&lt;/em&gt; action writes multiple representations of the same data at once (for backward compatibility some apps also write legacy non-UTI identifiers such as &lt;code&gt;NeXT Rich Text Format v1.0 pasteboard type&lt;/code&gt;). That’s how, for instance, if you copy from Pages and paste it into &lt;a href="https://apps.apple.com/app/id1669953820"&gt;MarkEdit&lt;/a&gt; — you get just the text, but if you paste it into TextEdit — you get the whole shebang.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/paste-to-apps.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a general rule, editors pick whatever is the &lt;em&gt;richest&lt;/em&gt; format they can handle. Some apps provide ways to force a specific format to be used. For example, a common menu item in the &lt;em&gt;Edit&lt;/em&gt; menu of rich text editors is &lt;em&gt;Paste and Match Style&lt;/em&gt; or &lt;em&gt;Paste as Plain Text&lt;/em&gt;. 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 &lt;em&gt;typing attributes&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;Another fun fact is that RTF is basically the serialized form of &lt;code&gt;NSAttributedString&lt;/code&gt;. Or vice versa, &lt;code&gt;NSAttributedString&lt;/code&gt; is the programmatic interface for RTF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSAttributedString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSAttributedString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alloc&lt;/span&gt; &lt;span class="nf"&gt;initWithString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;@"The quick brown fox jumps over the lazy dog."&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;NSData&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;dataFromRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="nl"&gt;documentAttributes:&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;
  &lt;span class="nl"&gt;NSDocumentTypeDocumentOption:&lt;/span&gt; &lt;span class="n"&gt;NSRTFTextDocumentType&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// {\rtf1\ansi\ansicpg1252\cocoartf2759…&lt;/span&gt;
&lt;span class="n"&gt;NSLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"%@"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alloc&lt;/span&gt; &lt;span class="nf"&gt;initWithData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Now, as I mentioned in the last chapter, this is all great for public UTIs. But what about &lt;a href="https://daringfireball.net/linked/2011/08/05/markdown-uti"&gt;semi-public&lt;/a&gt; ones like Markdown? From my experience, the cross-app exchange is a mixed bag…&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;public.text&lt;/code&gt; and the rich text representation as &lt;code&gt;public.rtf&lt;/code&gt;. When pasting to the second editor, it will pick &lt;code&gt;public.rtf&lt;/code&gt; 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 — “&lt;em&gt;I copy Markdown from here and paste it here — it should just copy 1:1&lt;/em&gt;”, but under the hood there is a lot of needless conversion.&lt;/p&gt;

&lt;p&gt;For this to work nicely, both apps should magically agree to export the &lt;code&gt;net.daringfireball.markdown&lt;/code&gt; UTI and prefer it over &lt;code&gt;public.rtf&lt;/code&gt;. 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 &lt;em&gt;does&lt;/em&gt; prefer &lt;code&gt;net.daringfireball.markdown&lt;/code&gt; over &lt;code&gt;public.rtf&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;“&lt;em&gt;But why export RTF at all? Markdown is all about plain text — drop RTF and problem solved&lt;/em&gt;” — 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 &lt;em&gt;should&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/paper-rich-text.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another feature closely related to the pasteboard is &lt;em&gt;sharing&lt;/em&gt; 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 &lt;a href="https://stackoverflow.com/questions/28654593"&gt;kind of are&lt;/a&gt; through some bizarre scripting language in a config file 😱). Rather, classes such as &lt;code&gt;NSAttributedString&lt;/code&gt;, &lt;code&gt;NSURL&lt;/code&gt;, and &lt;code&gt;UIImage&lt;/code&gt; 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 &lt;em&gt;share extension&lt;/em&gt; with a custom UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/internals-article/sharing.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  That’s it for now
&lt;/h2&gt;

&lt;p&gt;Check out the &lt;a href="https://papereditor.app/dev"&gt;first article&lt;/a&gt; if you haven’t already. It has a lot more tidbits about the app and the development process.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>ui</category>
      <category>ios</category>
      <category>design</category>
    </item>
    <item>
      <title>9 years of Apple text editor solo dev 👨🏻‍💻</title>
      <dc:creator>Mihhail Lapushkin</dc:creator>
      <pubDate>Tue, 09 Jan 2024 15:37:24 +0000</pubDate>
      <link>https://forem.com/mihhail/9-years-of-apple-text-editor-solo-dev-8hh</link>
      <guid>https://forem.com/mihhail/9-years-of-apple-text-editor-solo-dev-8hh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I tried my best to convert it from the &lt;a href="https://papereditor.app/dev"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;⌘+Click on the &lt;strong&gt;DEMO&lt;/strong&gt; links to view the videos.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In 2015 &lt;a href="https://twitter.com/_mihhail"&gt;I&lt;/a&gt; was a regular full-stack web developer (and still am to this day). I’ve owned a Mac. I’ve built a tiny iOS app once. That was about the extent of my proximity to the world of Apple dev at the time.&lt;/p&gt;

&lt;p&gt;Having spent some time behind a Mac, I’ve grown fond of its quirky and vibrant ecosystem of indie apps. One day, after stumbling upon a very simple and elegant Markdown editor called &lt;a href="https://ia.net/writer?ref=papereditor.app/dev"&gt;iA Writer&lt;/a&gt;, for one reason or the other, I decided to make something similar.&lt;/p&gt;

&lt;p&gt;Armed with enthusiasm, I started learning how to make a native text editor for the Mac. Xcode, AppKit, Objective-C — all of it was new to me and not something I would ever find use for at my day job. I had to learn a completely different tech stack that would live inside my head parallel to all the web knowledge.&lt;/p&gt;

&lt;p&gt;At some point, I started calling the app &lt;em&gt;Paper&lt;/em&gt; because, in pursuit of ultimate minimalism, I’ve reduced the editor to nothing more than a blank rectangle. To top it off, I’ve made the corners 90° instead of the typical rounded ones. Silly? Maybe… But it was my app so I could do whatever I wanted. 😈&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/paper.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In January 2017, 2 years after starting from ground zero, I launched the Mac app on the Mac App Store. The iOS app followed in 2019.&lt;/p&gt;

&lt;p&gt;Now, it’s not every day that a random, unknown web developer decides to build an app in a ridiculously crowded category, in a tech stack they have no experience with, and then actually does it, carrying on for the next 7 years. “&lt;em&gt;There’s gotta be some good material here&lt;/em&gt;” — I thought to myself.&lt;/p&gt;

&lt;p&gt;And so — here is a brain dump of all the weird, bizarre, and occasionally smart ways that I’ve organized my dev process, app architecture, and product philosophy, coming from a web developer who has not earned a cent working at a job as a Mac or an iOS developer but has earned quite a few of them selling a native text editor to the users of Apple devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why native?
&lt;/h2&gt;

&lt;p&gt;You could make the argument that an Electron app would work as well. Why go through the hassle of learning a brand new tech stack especially when my main job is web-related? I could have reused the skills, saved time, and supported more platforms all at the same time.&lt;/p&gt;

&lt;p&gt;Well, my goal was to deliver the best experience possible. I was trying to compete with highly polished writing apps, thus my app had to be light and fast to begin with. In addition to that, there are simply more ways to mess with the app on the native level — to make it unique (especially when it comes to text). I was not trying to reach the maximum number of users nor to cut down the development time. I had all the time in the world. I was trying to craft an experience that starts with lightning-fast download time and carries on into a native-feeling UI and UX.&lt;/p&gt;

&lt;p&gt;I wanted the best and I was willing to pay the penalty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Objective-C?
&lt;/h2&gt;

&lt;p&gt;In 2015 Swift was just getting started. I decided to make a test. I’ve compiled an empty Xcode project in Objective-C and another one in Swift and then examined the respective &lt;code&gt;.app&lt;/code&gt; packages. To my surprise, the Swift one had the full Swift runtime embedded into it — about &lt;code&gt;5MB&lt;/code&gt;, while the Objective-C one was super light — tens or maybe &lt;code&gt;100KB&lt;/code&gt; in total. That was enough to convince me to go with Objective-C.&lt;/p&gt;

&lt;p&gt;Again — I wanted the best and I was willing to pay the penalty of a harder-to-learn, soon-to-be outdated language to get a slimmer distributable.&lt;/p&gt;

&lt;p&gt;To be fair, if you run this experiment today the difference will not be that dramatic. Swift has come a long way and is now &lt;a href="https://belkadan.com/blog/2022/10/Swift-in-the-OS/"&gt;part of every platform&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third-party dependencies
&lt;/h2&gt;

&lt;p&gt;Paper does not have third-party dependencies.&lt;/p&gt;

&lt;p&gt;I had little trust in my ability to pick the right dependencies from an ecosystem that I was not familiar with. Plus by building everything on my own I could tailor it to my needs, gaining a slight advantage over competitors who often rely on external dependencies even for core parts of their apps.&lt;/p&gt;

&lt;p&gt;For example, the Markdown parsing engine in Paper is bespoke. Why is that a good thing? Because Paper supports less Markdown syntax than the traditional fully-fledged Markdown editor. I can code in just the right amount of parsing logic and nothing more. In addition to that, I can parse it with the right level of &lt;em&gt;metadata granularity&lt;/em&gt; which makes implementing features such as highlighting and text transformations simpler and more efficient. I took the same route with the &lt;code&gt;.docx&lt;/code&gt; export, spending several weeks unzipping &lt;code&gt;.docx&lt;/code&gt; files generated by Pages and Word, investigating the &lt;code&gt;.xml&lt;/code&gt; files inside, and then writing a simple Markdown to &lt;code&gt;.docx&lt;/code&gt; converter. Turns out the &lt;code&gt;.docx&lt;/code&gt; format is quite straightforward, and now I have both the knowledge and a tiny, easily supportable module that does exactly what I need.&lt;/p&gt;

&lt;p&gt;A similar but even stricter approach applies to UI components. Paper uses only native UI elements from AppKit and UIKit since they have the lowest maintenance overhead: auto-updated by Apple, adjustable to various &lt;a href="https://developer.apple.com/documentation/uikit/uitraitcollection"&gt;traits&lt;/a&gt;, backward compatible, and guaranteed to work on every device. Not to mention that to the average user, it is the most familiar UI — from the way it works to the &lt;em&gt;bounciness&lt;/em&gt; of animations.&lt;/p&gt;

&lt;p&gt;In case not a single built-in UI component is suitable to implement the desired feature, then I simply don’t add the feature. For instance, the &lt;code&gt;NSPopover&lt;/code&gt; is a good candidate for &lt;em&gt;bubbles that hint at stuff&lt;/em&gt; in the Mac app. An iOS counterpart sadly does not exist (&lt;a href="https://developer.apple.com/documentation/tipkit"&gt;TipKit&lt;/a&gt; is Swift-exclusive 😢), so no bubbles in the iOS app. 🤷🏼‍♂️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/caret-hint.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There were quite a few times when relying on auto-updated native components resulted in Paper getting free stylistic and feature updates that played to its strengths. Here are some of them that appeared in various versions of iOS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigation bar turns transparent if the scroll position is 0

&lt;ul&gt;
&lt;li&gt;Minimalism&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Buttons with &lt;a href="https://developer.apple.com/sf-symbols/"&gt;SF Symbols&lt;/a&gt; animate on press

&lt;ul&gt;
&lt;li&gt;Delight&lt;/li&gt;
&lt;li&gt;Native feel&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/uikit/app_and_environment/building_a_desktop-class_ipad_app#3966745"&gt;Document title menu&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Minimalism&lt;/li&gt;
&lt;li&gt;Native feel&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Find and Replace&lt;/em&gt; support in the text view component

&lt;ul&gt;
&lt;li&gt;Delight&lt;/li&gt;
&lt;li&gt;Native feel&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/free-os-features.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vision
&lt;/h2&gt;

&lt;p&gt;The initial vision for Paper was simple — build something that has the core tricks of iA Writer, but in a package that feels even more elegant and minimal. To achieve the desired effect I went all-in on cutting down distractions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not having a single extra button inside the app window.&lt;/li&gt;
&lt;li&gt;Ditching the standard Preferences window in favor of menu items and menu widgets.&lt;/li&gt;
&lt;li&gt;Hiding rarely used menu items under the ⌥ key.&lt;/li&gt;
&lt;li&gt;Slimming down the scroll bar to a 2px line that runs on the edge of the window and matches the 2px caret.&lt;/li&gt;
&lt;li&gt;Centering characters within the line so that there is an equal amount of caret above and below the character.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Scrolling away&lt;/em&gt; the title bar and extending the text area into the title bar (and making the remaining whitespace around the editor draggable to account for the fact that the usual &lt;em&gt;draggable&lt;/em&gt; title bar area can be taken by the editor).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/distraction-free.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have people noticed the effort? Most probably not… but some have. So much so that at one point Paper received a perfectly succinct review that I use as a tagline to this day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a super-clean writing space with a lot of configurability that stays out of sight when you don’t need it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As time went on I started developing a feeling for how the market of minimal Apple text editors looks like and what could be Paper’s place in it.&lt;/p&gt;

&lt;p&gt;To my observations, minimalist writing apps usually follow 2 paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Become popular and start slowly drifting away from their minimalist roots to satisfy the ever-growing demands of mainstream users.&lt;/li&gt;
&lt;li&gt;Remain too simple and niche to eventually be abandoned by their creators.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Paper is not [1.] but it could be on the path to [2.].&lt;/p&gt;

&lt;p&gt;My plan is to forever keep the app as minimal as it was when it launched — to resist adding any visual clutter. For a certain group of people, this is a vital requirement that other apps (apparently) fail to address.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please do not make Paper more complex! There are plenty and plenty of 'full featured' editors out there, and they do not fit the bill for focused writing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the same time, by having a slow and predictable cadence of small updates (more on that at the end) I can slowly add features to the &lt;em&gt;fringes&lt;/em&gt; of the app while keeping the &lt;em&gt;default path&lt;/em&gt; super clean. Making Paper more useful but not bloated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgzg925uir8uq7loon8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgzg925uir8uq7loon8p.png" alt="An illustration for the above-mentioned separation of “fringes” and “default path”. In the middle is a big arrow pointing down labeled “default path. It has a bunch of screenshots of the Mac app in it with the default UI state and default menu items. On the sides labeled “fringes” there are a bunch of screenshots with advanced features and additional menu items that become visible when the Option key is pressed." width="800" height="1044"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The slow pace and the overall limited number of features allow me to focus on building a better foundation, to better understand how things work together, and to avoid adding features that bring instability and a high maintenance burden in the future.&lt;/p&gt;

&lt;p&gt;Closed-sourced native UI is a fragile place compared to the predictable JavaScript runtime of the browser. If you don’t invest substantial resources into refactoring your app and eliminating bugs — it’s death by a thousand crashes. And this is what I am banking on with regard to [1.]. The bloat, complexity, and bugs that [1.] accumulate from their decision to go mainstream present good opportunities to capture some of the disappointed users that eventually leave them.&lt;/p&gt;

&lt;p&gt;This however may not be enough to make Paper into a viable product. There are simply not enough (reachable) people who need these kinds of ultra-simple writing apps (let alone pay for one). Power users are the ones who pay the bills because they need power tools to earn money that they can then justify spending on those tools. And while [1.] and [2.] are the majority, there &lt;em&gt;definitely&lt;/em&gt; are cases of simple writing apps that remain simple and/or that are still supported by obsessive single players like me. There might just never be a big enough gap in the market for Paper. 🤷🏼‍♂️&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;I find it convenient to think of Paper’s code as consisting of two &lt;em&gt;scopes&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Global stuff that exists as a single instance or is applied to the app as a whole.

&lt;ul&gt;
&lt;li&gt;macOS menu&lt;/li&gt;
&lt;li&gt;iOS status bar&lt;/li&gt;
&lt;li&gt;Touch bar&lt;/li&gt;
&lt;li&gt;App icon&lt;/li&gt;
&lt;li&gt;Dark mode&lt;/li&gt;
&lt;li&gt;Input language&lt;/li&gt;
&lt;li&gt;Global app config&lt;/li&gt;
&lt;li&gt;Global app logic&lt;/li&gt;
&lt;li&gt;Single instance views&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;A collection of views and logic that live inside a view controller representing a single document.&lt;/li&gt;
&lt;li&gt;A new document scope spawns into life the moment the document is opened and dies when it is closed.&lt;/li&gt;
&lt;li&gt;Unlike a singular application scope, zero or more document scopes can exist at the same time.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reality is a bit more nuanced as there are also &lt;em&gt;scenes&lt;/em&gt; on iOS that subdivide the global scope, but the mental model holds more or less.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/modules.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For every scope, I define a &lt;em&gt;storyboard&lt;/em&gt; that serves 2 functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It describes various views and widgets that are used within the scope.&lt;/li&gt;
&lt;li&gt;It acts as a &lt;a href="https://en.wikipedia.org/wiki/Dependency_injection"&gt;dependency injection&lt;/a&gt; container that glues together all &lt;em&gt;modules&lt;/em&gt; within the scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/storyboard.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modules (not to be confused with &lt;code&gt;.modulemap&lt;/code&gt; stuff — I just happen to use the same name) in Paper are plain Objective-C classes that take responsibility for a piece of functionality within the app. It’s a way to group functionality related to a particular feature instead of spreading it across multiple places.&lt;/p&gt;

&lt;p&gt;Modules have a well-defined lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They are created by the storyboard when the &lt;em&gt;main thing&lt;/em&gt; within the storyboard gets created.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;main thing&lt;/em&gt; is the app itself, so all modules are created on startup.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;main thing&lt;/em&gt; is the view controller that holds the editor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;They have a setup method that gets called after the dependencies have been injected, but before the &lt;em&gt;main thing&lt;/em&gt; becomes visible.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Called on

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ApplicationDidFinishLaunchingNotification&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UISceneWillConnectNotification&lt;/code&gt; for scenes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Called on &lt;code&gt;didMoveToWindow&lt;/code&gt; of the main view.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;They have a tear-down method that gets called before the &lt;em&gt;main thing&lt;/em&gt; is destroyed.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;No need. Modules die with the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document scope&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Called on &lt;code&gt;willMoveToWindow&lt;/code&gt; of the main view when the &lt;code&gt;newWindow&lt;/code&gt; argument is &lt;code&gt;nil&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Modules declare their dependencies — views and other modules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;@interface&lt;/span&gt; &lt;span class="nc"&gt;TvTextAttributeModule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;IBOutlet&lt;/span&gt; &lt;span class="n"&gt;NSTextView&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;textView&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;IBOutlet&lt;/span&gt; &lt;span class="n"&gt;TvTextContentModule&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tvTextContentModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;IBOutlet&lt;/span&gt; &lt;span class="n"&gt;TvLayoutAttributeModule&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tvLayoutAttributeModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;IBOutlet&lt;/span&gt; &lt;span class="n"&gt;TvCaretModule&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tvCaretModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;IBOutlet&lt;/span&gt; &lt;span class="n"&gt;TvTypewriterModeModule&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tvTypewriterModeModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I resolve them manually in Xcode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/connect-outlet.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dependencies are then injected by the storyboard at runtime when the view controller is instantiated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;UIStoryboard&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UIStoryboard&lt;/span&gt; &lt;span class="nf"&gt;storyboardWithName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;@"Document"&lt;/span&gt;
                                             &lt;span class="nl"&gt;bundle:&lt;/span&gt;&lt;span class="nb"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;UINavigationController&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="nf"&gt;instantiateInitialViewController&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a document-scoped module needs something from the application scope it can get it from the global variable…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt; &lt;span class="n"&gt;unread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootViewController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supportChatModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unread&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;UIImage&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UIImage&lt;/span&gt; &lt;span class="nf"&gt;systemImageNamed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;@"gear"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;UIImage&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;unreadIcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UIImage&lt;/span&gt; &lt;span class="nf"&gt;systemImageNamed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;@"gear.badge"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rightBarButtonItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unread&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;unreadIcon&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modules subscribe to notifications (pub-sub events between classes) during the setup phase of the lifecycle and unsubscribe during the tear-down phase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;addNotificationObservers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;@selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windowDidResize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;NSWindowDidResizeNotification&lt;/span&gt;
                   &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;@selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textViewDidChangeSelection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;NSTextViewDidChangeSelectionNotification&lt;/span&gt;
                   &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textView&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;@selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colorsDidChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;didChangeNotification&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modules can let other modules know that something has happened. This is done through direct method calls rather than notifications to keep things simpler, IDE navigable, and faster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;UIEdgeInsets&lt;/span&gt; &lt;span class="n"&gt;contentInset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calculateContentInset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;UIEdgeInsets&lt;/span&gt; &lt;span class="n"&gt;indicatorInsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calculateScrollIndicatorInsets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentInset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contentInset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verticalScrollIndicatorInsets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;indicatorInsets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;docFrameModule&lt;/span&gt; &lt;span class="nf"&gt;textViewDidChangeInsets&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTypewriterModeModule&lt;/span&gt; &lt;span class="nf"&gt;textViewDidChangeInsets&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modules can ask other modules to do or to calculate something that those other modules are responsible for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;textViewDidAnimateCaretFirstAppearance&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shouldCancel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="nf"&gt;centerIn_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;docFrameModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleFrame&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;animateIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Delay_&lt;/span&gt; &lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:^&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;animateOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="nf"&gt;removeFromSuperview&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}];&lt;/span&gt;
    &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;}];&lt;/span&gt;

  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This leaves the delegates, view controllers, and views as proxies that notify modules about stuff that’s going on in the app and delegate everything to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;insertText&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;replacementRange&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSRange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;range&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvAutocompleteModule&lt;/span&gt; &lt;span class="nf"&gt;textViewWillInsertText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTypewriterModeModule&lt;/span&gt; &lt;span class="nf"&gt;textViewWillInsertText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTextAttributeModule&lt;/span&gt; &lt;span class="nf"&gt;textViewWillInsertText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;super&lt;/span&gt; &lt;span class="nf"&gt;insertText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;replacementRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTextAttributeModule&lt;/span&gt; &lt;span class="nf"&gt;textViewDidInsertText&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTypewriterModeModule&lt;/span&gt; &lt;span class="nf"&gt;textViewDidInsertText&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All in all, the combination of modules and dependency injection through storyboards gives a nice mechanism to decouple and subdivide what would have been a giant document view controller and/or text view. Applying the same approach to the application scope gives the whole app a uniform structure that is a joy to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform code
&lt;/h2&gt;

&lt;p&gt;AppKit and UIKit are both quite similar and annoyingly different in many places.&lt;/p&gt;

&lt;p&gt;I employ 2 Objective-C features to work around the differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Macros (this one is inherited from C)&lt;/li&gt;
&lt;li&gt;Categories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, in many cases, the difference comes down to &lt;code&gt;NS&lt;/code&gt; vs &lt;code&gt;UI&lt;/code&gt; prefix which I mitigate with this macro.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if TARGET_OS_OSX
#define KIT(symbol) NS##symbol
#else
#define KIT(symbol) UI##symbol
#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, whenever I need to declare a view in a shared module I can simply type &lt;code&gt;KIT(View)&lt;/code&gt; instead of &lt;code&gt;NSView&lt;/code&gt; or &lt;code&gt;UIView&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;cloneCaretView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;KIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;createCaretView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;caretFrame&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt; &lt;span class="nf"&gt;setNeedsDisplay&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides that, I can use those &lt;code&gt;#if&lt;/code&gt; statements to quickly add platform-specific code in shared classes. To make such code more readable Xcode even has a cool trick that deemphasizes the code that does not apply to the currently selected platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/deephasize-macros.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Categories in Objective-C are a way to add new methods to any existing class, including framework classes (categories can also be used to replace methods, which is both powerful and scary 😵). I use them to harmonize the API. So if a method in &lt;code&gt;UITextField&lt;/code&gt; is called &lt;code&gt;text&lt;/code&gt; and in &lt;code&gt;NSTextField&lt;/code&gt; it is called &lt;code&gt;stringValue&lt;/code&gt; I can add a &lt;code&gt;stringValue&lt;/code&gt; method to &lt;code&gt;UITextField&lt;/code&gt; that calls &lt;code&gt;text&lt;/code&gt; (or vice versa). Now I can always refer to the &lt;code&gt;stringValue&lt;/code&gt; method in my &lt;code&gt;KIT(TextField)&lt;/code&gt; variable and it will compile on both platforms.&lt;/p&gt;

&lt;p&gt;I gather all these little patches to an &lt;em&gt;adapter&lt;/em&gt; header in the shared library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;@interface&lt;/span&gt; &lt;span class="nc"&gt;UITextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;SharedApplicationAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonatomic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stringValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonatomic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;placeholderString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then implement this header in respective app projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;@implementation&lt;/span&gt; &lt;span class="nc"&gt;UITextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;SharedApplicationAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;stringValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;setStringValue&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;stringValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stringValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;placeholderString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;setPlaceholderString&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSString&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;placeholderString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;placeholderString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the way, I also use categories to shorten long framework methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;@implementation&lt;/span&gt; &lt;span class="nc"&gt;UIViewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Mobile_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;dismiss_&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;dismissViewControllerAnimated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;YES&lt;/span&gt; &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The underscore at the end helps to avoid clashes with public or private methods that Apple might decide to add in the future. Apple often prefixes private methods with an underscore, so having it at the end makes more sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;Throughout my career I’ve always learned the quickest and the most about a particular third-party dependency by reading the code rather than going through the docs. Documentation is fine to get a general picture, but the details matter especially when stuff does not work the way it should either by (undocumented) design or due to mistakes of the authors.&lt;/p&gt;

&lt;p&gt;Now in the case of Apple frameworks, documentation is sadly the only readable option you have. A far less readable one is to put a breakpoint in your code and examine the compiled stack trace of the framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T9AMAriJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack-breakpoint.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T9AMAriJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack-breakpoint.png" alt="A call stack in Xcode on the left. One frame is selected. The right pane shows the machine code of the selected frame." width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It looks like gibberish, but the important thing is that the method names are visible. Analyzing the flow of method calls is enough to grasp what is going on. And if you need to inspect something that is outside the stack ending in your source code you can always add a &lt;em&gt;symbolic breakpoint&lt;/em&gt; to stop inside the framework. For instance, below it stops at &lt;code&gt;recognizedFlickDirection&lt;/code&gt; which is visible at the top of the compiled code above.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--u395HjMM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack-symbolic-breakpoint.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u395HjMM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack-symbolic-breakpoint.png" alt="A call stack in Xcode on the left. One frame is selected. The right pane shows the machine code of the selected frame." width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a side note, going through a 30-year-old codebase you occasionally discover gems such as &lt;code&gt;if&lt;/code&gt; statements adjusting the logic of the framework depending on the (Apple) app that runs the framework, or instances of creative naming. 🙃&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UHmTOw-7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UHmTOw-7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/call-stack.png" alt="A call stack in Xcode. The top frame is “reallySendEvent”. One below it is “sendEvent”." width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paid features
&lt;/h2&gt;

&lt;p&gt;Back in 2015–17, subscriptions were not a widespread thing yet in the App Store. Pay-to-download was (and is) common, but I did not think that someone would pay for an unknown app without trying it, so the one-time payment freemium seemed like the only option. At the same time, I did not want to just paywall the features or implement a custom time-based trial (only subscriptions have built-in trials in the App Store). I wanted something extremely user-friendly. Something that feels like you can push all the buttons and adjust all the toggles of the paid offering without limits, without needing to explicitly commit to a trial of arbitrary length.&lt;/p&gt;

&lt;p&gt;My solution was to offer only &lt;em&gt;cosmetic&lt;/em&gt; upgrades as part of the Pro offering — visual changes rather than functional features (e.g. file syncing or PDF exports).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/pro-features.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would then make them &lt;em&gt;trialable&lt;/em&gt; for an unlimited amount of time. Users could &lt;em&gt;test drive&lt;/em&gt; the features to see how they look and work and then buy the Pro offering if they thought that the features were worth it.&lt;/p&gt;

&lt;p&gt;Of course, there needed to be a measure that prevented people from using the features indefinitely without paying. At first, it was a 60-second timer that would nag them to buy if at least one of the Pro features was active. This proved to be too annoying, so I tied the nagging to characters written instead. It made more sense — try everything for as much time as you want without distractions but as soon as you start using Paper to write then every couple of hundred characters you would see a popup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oxGhwjkJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/trial-popup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oxGhwjkJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/trial-popup.png" alt="A Mac alert popup that says “Enjoying Pro Features? 😎 You are using some of the Pro Features from the View menu. 👀 Consider Subscribing if you think they are worth it. 💳 Press Reset to reset Pro Features to defaults and get rid of this popup. 👻”. Three buttons: Subscribe, Ask Again Later, Reset." width="744" height="994"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why does this work only for cosmetic upgrades? Because, unlike functional features whose value is tied to a specific momentary action, cosmetic upgrades enhance your writing experience on a constant basis. You cannot put a functional feature behind the same frictionless trial described above since the value of the feature is fully realized the moment the action is performed. On the other hand, letting the users apply cosmetic changes accompanied by a nagging mechanism gives them a taste of the enhanced writing experience yet limits the prolonged value of this enhancement. Forcing the purchase, yet not limiting the ability to fully explore everything on their terms before spending the money.&lt;/p&gt;

&lt;p&gt;What do users think about it? Here is an excerpt from a recent review.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;…the fact that it lets you mess around with and test pro features instead of locking them behind a paywall (a reason I do not like a lot of apps. They don’t let you try the features until you start a subscription or trial you may very well forget about, if it even gives you a trial) is extremely nice. […] You don’t even have to necessarily trust me on it: jump in and try it yourself. It lets you test every single feature it has.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Paper started with 2 one-time payments of $5 each for 2 sets of Pro Features. Today, after many experiments, it is $10 per month or $100 lifetime for a single set.&lt;/p&gt;

&lt;p&gt;First, before plunging into the world of subscriptions, I experimented with one-time payments. I started gradually doubling the price and observing if the total amount of money earned would increase or decrease. My App Store traffic was pretty stable, so there were always new people coming through the door who did not know anything about previous prices. That’s when I first discovered that people were willing to pay up to $100 for an app from an unknown developer. I tried going up to $200. I think I’ve gotten like 1 or 2 sales and some amount of complaints over several months so I figured that $200 is probably the pain point. Thus the market has decided that $100 is an adequate price for Paper.&lt;/p&gt;

&lt;p&gt;Next after a lot of hesitation, I decided to introduce subscriptions. “&lt;em&gt;No one would pay a subscription for such a simple app&lt;/em&gt;” — was my conviction. But I was wrong. They worked and today Paper has a healthy mix of monthly, annual, and lifetime payments with subscriptions being the default, promoted payment option (since SaaS is all the rage, right?).&lt;/p&gt;

&lt;p&gt;Even now I still experiment with prices. Recently I doubled the monthly price for new subscribers in some countries while keeping the annual price unchanged. This is to incentivize the annual option and to reduce churn. Preliminary results show that people continue subscribing though in smaller numbers. Still — this could be a positive change taking into account the higher monthly price and annual upgrades. We’ll have to wait and see…&lt;/p&gt;

&lt;p&gt;What’s also great about the App Store is that you can easily test lowering prices for countries with lower incomes. Since Apple does not charge the typical ¢50 per transaction (e.g. like &lt;a href="https://www.paddle.com/pricing"&gt;Paddle&lt;/a&gt;) you can go as low as you want without those fees eating into your margins. Currently, I am testing to see if lowering the price has any effect or if people in those countries simply don’t pay for software (or text editors 🤷🏼‍♂️) no matter what.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gnarly stuff
&lt;/h2&gt;

&lt;p&gt;Text editors are hard…&lt;/p&gt;

&lt;p&gt;Text is one of those things that always gets things bolted onto it. Copy-paste, drag and drop, undo, caret interactions, right-to-left languages, non-alphabetic languages, dictation, spoken text, scanned text, non-text objects in text, data format recognizers, tap-and-hold link previews, text search overlay, spell check, autocorrection, autocomplete, AI suggestions — it never stops. You’re always at the mercy of the next OS update adding new ways to insert, update, and interact with editable text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/text-features.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figuring out the math behind the text editor rectangle is particularly tricky on iOS with various factors like the Dynamic Island (or the notch), the Home Bar, and the dynamically appearing software keyboard, times 2 orientations, getting in the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---AohktwX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/text-editor-math.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---AohktwX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/text-editor-math.png" alt="Breakdown of the iPhone app text editor structure." width="800" height="1457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Input languages and fonts are a headache as well. Alphabetic languages are relatively easy to deal with as most major fonts support even quirky glyphs like &lt;a href="https://en.wikipedia.org/wiki/Umlaut_(diacritic)"&gt;umlauts&lt;/a&gt; and Cyrillics. The non-alphabetic languages however need specific fonts to display their glyphs. Luckily Apple’s systems come with at least one of those fonts preinstalled per every non-alphabetic input language. The only problem is that no API maps an input language to supported fonts — so I had to brute force it. I’ve installed every input language on my Mac and wrote some text in each of them in TextEdit to see what font TextEdit falls back to when it sees the typed glyphs. The following &lt;code&gt;switch&lt;/code&gt; is the result of it (I am pretty sure I’ve messed up the names of some languages or nationalities — don’t judge me too hard, I am just a developer 😩).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputLanguage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Alphabetic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fontName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Courier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Courier"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_CourierPrime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"CourierPrime"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_CourierPrimeSans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"CourierPrimeSans-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Menlo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Menlo-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Helvetica&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Helvetica-Light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Avenir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"AvenirNext-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_NewYork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NewYork-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Georgia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Georgia"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_TimesNewRoman&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"TimesNewRomanPSMT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Palatino&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Palatino-Roman"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_BrushScript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"BrushScriptMT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;FontName_Charter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Charter-Roman"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Japanese&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Cantonese&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"HiraginoSans-W2"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_ChineseSimplified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"PingFangSC-Light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_ChineseTraditional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"PingFangTC-Light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Korean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"AppleSDGothicNeo-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Arabic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"SFArabic-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Thai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Thonburi-Light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Hebrew&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"SFHebrew-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Hindi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"KohinoorDevanagari-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Bengali&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"KohinoorBangla-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Malayalam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"MalayalamSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Burmese&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"MyanmarSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Gujarati&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"KohinoorGujarati-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Kannada&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansKannada-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Oriya&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansOriya"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Telugu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"KohinoorTelugu-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Gurmukhi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"GurmukhiSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Sinhala&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"SinhalaSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Khmer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"KhmerSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Tibetan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Kailasa"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Armenian&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"SFArmenian-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Georgian&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"SFGeorgian-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Tamil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"TamilSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Amharic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Kefa-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Syriac&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansSyriac-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Cherokee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"Galvji"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Dhivehi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansThaana-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Adlam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansAdlam-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Hmong&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansPahawhHmong-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Inuktitut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"EuphemiaUCAS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Lao&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"LaoSangamMN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Mandaic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansMandaic-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_MeeteiMayek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansMeeteiMayek-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_NKo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansNKo-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Osage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansOsage-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Rejang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansRejang-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_HanifiRohingya&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansHanifiRohingya-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_OlChiki&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansOlChiki-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Tifinagh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansTifinagh-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Wancho&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"NotoSansWancho-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputLanguage_Punjabi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;@"MuktaMahee-Regular"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then detect the selected input language and set the font accordingly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0V9AhC5A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/non-alphabetic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0V9AhC5A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/non-alphabetic.png" alt="A screenshot of Font settings in the iPhone app. The only font available and selected is “Apple SD Gothic Neo”. The label above the font name says “Korean”. The gray text under the font name reads “Paper has automatically switched to the most optimal font for your keyboard language.”." width="750" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more thing worth mentioning is the fiddly logic of batched text updates e.g. shortcuts that toggle Markdown formatting on a selected piece of text. A naive solution is straightforward, but coding for all the edge cases to avoid breaking the Markdown can be a pain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A5rcVoys--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/markdown-formatting.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A5rcVoys--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/markdown-formatting.png" alt="Breakdown of the way Bold Markdown formatting shortcut works in Paper." width="800" height="1794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gimmicks
&lt;/h2&gt;

&lt;p&gt;A &lt;em&gt;thing&lt;/em&gt; I’ve picked up from the amazing &lt;a href="https://culturedcode.com/things/?ref=papereditor.app/dev"&gt;Things&lt;/a&gt; app is the delightful resize bounce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/resize-bounce.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a lot of trial and error, this is the damping logic that I’ve landed on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;NSRect&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMinWidthReached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthReached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthReached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;YES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthMouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouseLocation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMinHeightReached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightReached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightReached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;YES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightMouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouseLocation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;damp&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fabs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;mouseDeltaX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouseLocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthMouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;mouseDeltaY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouseLocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightMouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;minWidthFrameX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSMinX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minWidthFrame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;minHeightFrameY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSMinY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minHeightFrame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;dampedX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;minWidthFrameX&lt;/span&gt; &lt;span class="k"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;damp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mouseDeltaX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;dampedY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;minHeightFrameY&lt;/span&gt; &lt;span class="k"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;damp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mouseDeltaY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="nf"&gt;setFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NSMakeRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMinWidthReached&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;dampedX&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NSMinX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isMinHeightReached&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;dampedY&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NSMinY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;NSWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;NSHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;YES&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part was figuring out the combination of math functions that yield a realistic rubber band effect. 🤓&lt;/p&gt;

&lt;p&gt;Another fun mathy feature in Paper is the &lt;em&gt;rotate-to-undo&lt;/em&gt; gesture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/rotate-undo.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of calculations are involved to make it happen, but at the core of it is this logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frameCenterRotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelViewRotation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSUInteger&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;CircleView&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;undoItemView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subviews&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;bottomPointPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="n"&gt;undoItemView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;bottomPointPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="n"&gt;undoItemView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;affineTransform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CGAffineTransformMakeScale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scale&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;shouldWheelItemBeFilled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;undoItemView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;wheelItemFillColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;undoItemView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;clearColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;bottomPointPercent&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;wheelItemIndex&lt;/span&gt;
                             &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;zeroRange&lt;/span&gt;
                             &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CGFloat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;maxRange&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelViewRotation&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;wheelItemIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rotation&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fabs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;zeroRange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;maxRange&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;maxRange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BOOL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;shouldWheelItemBeFilled&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSUInteger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;wheelItemIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;undoItemIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;wheelItemIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
         &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;undoItemIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;wheelItemIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;undoRedoCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;wheelItemFillColor&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSUInteger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;wheelItemIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt; &lt;span class="nf"&gt;undoItemIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;wheelItemIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redoCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;floatingElementHighlight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;floatingElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSInteger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;undoItemIndex&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;NSUInteger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;wheelItemIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;NSUInteger&lt;/span&gt; &lt;span class="n"&gt;wheelItemCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wheelView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subviews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;NSUInteger&lt;/span&gt; &lt;span class="n"&gt;highlightedIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redoCount&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;wheelItemCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;CGFloat&lt;/span&gt; &lt;span class="n"&gt;fullWheelTurns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redoCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;wheelItemCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;fullWheelTurns&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;wheelItemCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wheelItemIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;highlightedIndex&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;wheelItemCount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="n"&gt;wheelItemIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://arc.net/?ref=papereditor.app/dev"&gt;Arc&lt;/a&gt; used to use the phrase &lt;a href="https://twitter.com/disco_lu/status/1660347464124178440"&gt;&lt;em&gt;Gift Arc to a friend&lt;/em&gt;&lt;/a&gt; in their app-sharing feature — this felt right up Paper’s alley.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/gift.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have no idea if this next one has any effect on sales, but in any case — I show the total number of Pro users, the positive delta since the last time the user saw this number, and the annual plan savings (which may differ per country or change on-the-fly due to pricing experiments).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/pro-count.mp4"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The delta number is considered &lt;em&gt;seen&lt;/em&gt; when it becomes visible on the screen. So it can sit there, unseen for a long time, and then display a big delta once the user has scrolled to it.&lt;/p&gt;

&lt;p&gt;Moving on to the super gimmicky territory — this animation is triggered when the screen goes to sleep while Paper is in the foreground. 💤&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/zzz.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lastly — something I’ve noticed in the world of Mac apps is that despite the attention to detail in the app itself the About window almost always gets zero love. Why not make it a little bit more fun? 👨🏻‍🎨&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sJuhB_h5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/about.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sJuhB_h5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/about.png" alt="A small, white, 2:1 Mac window that contains a centered text saying “Crafted in Tallinn”. The text is light red in a cursive font. Below this text, there is a little gray number 61 in a regular font." width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tidbits
&lt;/h2&gt;

&lt;p&gt;I’ve gathered a few smaller details that did not fit into other chapters but are worthy of mentioning nonetheless.&lt;/p&gt;

&lt;p&gt;Details such as the disappearing hints in menus. The idea stemmed from the need to explain certain features or to hint at certain shortcuts in the menus. Like with many other things in Paper, my only constraint was — no extra (permanent) visual weight in the interface. To satisfy the constraint I decided to count the number of times the user has seen a particular hint and then never show it again after a certain number of views.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/menu-hints.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Speaking of reduced visual weight — sometimes a UI element needs to draw a bit more attention until it’s clicked, but after that, it should fade into the background to avoid distracting the user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/button-highlight.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next one came from the need to nudge people to do certain things in the Mac app. I asked myself — “&lt;em&gt;What would be the least annoying way to ask the user to do something while they are using the Mac app?&lt;/em&gt;”. The best idea that I came up with was to put it at the top of each menu. The thinking was that if you made your way into the menu, you anyway take a small mental break, so I would not be taking you out of your flow with my asks. I also made sure to show the nudge only after a certain period of active usage of the app, and with a delay between the previous nudge, so they would not appear too often.&lt;/p&gt;

&lt;p&gt;What kind of nudges are those? At various stages of Paper’s life, these included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Letting them know that holding the ⌥ key reveals more menu items.&lt;/li&gt;
&lt;li&gt;Letting them know that the mobile app exists.&lt;/li&gt;
&lt;li&gt;Asking to rate the app in the Mac App Store.&lt;/li&gt;
&lt;li&gt;Polls with 1 question and multiple answers.

&lt;ul&gt;
&lt;li&gt;Removed. They worked great, but I ran out of things to ask at some point. 😄&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Asking to share the app on Twitter.

&lt;ul&gt;
&lt;li&gt;Removed. Did not work at all…&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/menu-nudges.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A fun technical side note — every time I need to store the state of a hint or a nudge or some other UI bit, I use the &lt;a href="https://developer.apple.com/documentation/foundation/icloud/synchronizing_app_preferences_with_icloud"&gt;ubiquitous key-value store&lt;/a&gt;. It’s an iCloud store that is linked to the user’s Apple ID, and where every App Store app can save small amounts of data that sync across the user’s devices. This way I can ensure that the user does not see the same UI bit twice on different devices (or fresh installations of the app).&lt;/p&gt;

&lt;p&gt;The final tidbit is the ultimate manifestation of the &lt;em&gt;fringes&lt;/em&gt; vs &lt;em&gt;default path&lt;/em&gt; approach. I have a hidden place in the app where I dump all the settings that are useful for (and were requested by) only a small group of users. I call this section &lt;em&gt;Nitpicking&lt;/em&gt;. By moving them out of sight, I am not cluttering the UI for the mainstream users while at the same time keeping the nitpicky users happy.&lt;/p&gt;

&lt;p&gt;In the Mac app, this is an alternate menu for the About menu item.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://papereditor.app/dev-article/nitpicking-mac.mov"&gt;&lt;strong&gt;DEMO&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In iOS, the Settings app felt like the perfect place for this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NgwhMKNe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/nitpicking-ios.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NgwhMKNe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/nitpicking-ios.png" alt="A screenshot of Paper settings inside the iPhone Settings app. These setting groups are visible: Markdown, Editing Permissions, Edit Menu Actions, Typing Noises, Analytics, Purchases, and Nitpicking." width="750" height="1294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback
&lt;/h2&gt;

&lt;p&gt;Back in the day, Paper was using an app lifecycle management platform called HockeyApp. The Mac version of HockeyApp came with a hosted support chat that you could easily launch from within your app. Because this chat was so accessible, people were leaving useful feedback in vastly greater numbers compared to email.&lt;/p&gt;

&lt;p&gt;Then came Microsoft, bought HockeyApp and turned it into AppCenter, sunsetting this feature in the process.&lt;/p&gt;

&lt;p&gt;After going back to the desert that is feedback via email I decided to build the support chat myself for both platforms and integrate it to be a beautiful part of the app experience rather than something &lt;em&gt;tacked on&lt;/em&gt; as an afterthought.&lt;/p&gt;

&lt;p&gt;I tried mimicking the aesthetic of the Messages app on respective platforms down to those little curvy tips on chat bubbles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rEHGzhG3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/support-chat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rEHGzhG3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/support-chat.png" alt="A Mac app chat window and an iOS app chat window. Both contain 2 chat bubbles. The first one is from Mihhail and contains the text “Hey, I make Paper. 👋 What’s on your mind? 🤔”. The second one is from the user and contains the text “Are you a bot?”." width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unlike HockeyApp which required an email to get started, I decided to lower the friction to zero to maximize the feedback potential. I was fine to trade lower entry barriers for more spam.&lt;/p&gt;

&lt;p&gt;For the backend, I wanted something hosted and free so I ended up saving conversations to &lt;code&gt;UUID.json&lt;/code&gt; files on Dropbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x2mr3mUc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/uuid-json.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x2mr3mUc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/uuid-json.png" alt="A part of the Mac Finder window with a list of UUID-named JSON files. File names are mostly blurred out." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I edit those files in a text editor and it syncs back to Dropbox. Looking back, I should have simply built it on top of a Slack workspace. Nevertheless, Dropbox has worked well and still works great for me.&lt;/p&gt;

&lt;p&gt;With time I even spotted a few clear patterns and coded the logic that looks for keywords both in the unsent message box and in the last sent message to show auto-replies as soon as the user has typed the needed keyword. Now I rarely get these questions — they are answered &lt;em&gt;before&lt;/em&gt; the send button is pressed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8_ZG3Xbl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/auto-replies.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8_ZG3Xbl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/auto-replies.png" alt="A chat window with 2 chat bubbles. The first one is from Mihhail and contains the text “Hey, I make Paper. 👋 What’s on your mind? 🤔”. The second one is also from Mihhail and contains the text “Are you asking about photos? 🤔 Sorry, Paper is for text only. Can’t add photos. PS: This is an auto-reply 🤖”. The unsent message box contains the text “Can I add photos?”." width="800" height="1383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oh, and of course I can’t answer at night. 😴&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SDcGfLhf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/sleeping.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SDcGfLhf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/sleeping.png" alt="A chat window with 2 chat bubbles. The first one is from Mihhail and contains the text “Hey, I make Paper. 👋 What’s on your mind? 🤔”. The second one is also from Mihhail and contains the text “Hey, I am sleeping right now 😴. Go ahead and leave me a message 💬. I’ll reply as soon as I can 👀.”." width="800" height="1383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the end, building a polished, frictionless support chat was one of the best decisions that I have ever made for Paper. Not only did it differentiate the app from the rest (unlike the web, live chats in native apps are not a thing, especially in tiny apps like Paper), but also resulted in happier (and often positively surprised) users, faster feedback in case of bugs, and a lot of great ideas. In fact, at some point, 90% of my roadmap started being informed by ideas from the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Releases
&lt;/h2&gt;

&lt;p&gt;So I’ve gathered and implemented the feedback from the chat. How are the features then propagated to the users?&lt;/p&gt;

&lt;p&gt;About once a month I release one flagship feature that goes into release notes. Often I prepare multiple features during a development streak and then drip them out one at a time over the next months. This gives me the highest chance that someone would actually read the single sentence that is present in the release notes every month.&lt;/p&gt;

&lt;p&gt;Bug fixes, tweaks, and smaller features go into releases usually without being mentioned. As a developer, you might feel better about putting “&lt;em&gt;We’ve fixed some bugs and made a few performance improvements&lt;/em&gt;” into your release notes, but the average user of a niche, unknown app like Paper has no patience to care.&lt;/p&gt;

&lt;p&gt;If an urgent fix is needed — I simply copy-paste the previous release notes to keep the flagship feature advertisement going. I do not mention the fix at all.&lt;/p&gt;

&lt;p&gt;For the version number, I use a single monotonically increasing number. It’s the simplest thing so why complicate it with 2+ numbers separated by dots?&lt;/p&gt;

&lt;p&gt;And here is the template that I use for the release notes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dear User,&lt;/p&gt;

&lt;p&gt;One sentence. Two–three in rare cases.&lt;/p&gt;

&lt;p&gt;Pleasant update,&lt;br&gt;
Your Paper&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A bit cringey, but I &lt;em&gt;kinda&lt;/em&gt; like it. 🫠 It’s like every month the app sends you a letter explaining what’s new. Some users have even mentioned this as a thing they look forward to.&lt;/p&gt;

&lt;p&gt;Having something to release every month be it small or big is what greases the flywheel of App Store distribution.&lt;/p&gt;

&lt;p&gt;It shows the current users that their subscription is worth it.&lt;/p&gt;

&lt;p&gt;It signals to potential users that the app has been recently updated.&lt;/p&gt;

&lt;p&gt;It tells the App Store algorithm that the app is not abandoned.&lt;/p&gt;

&lt;p&gt;And finally, this slow and steady pace is what allows me to keep this thing going for years to come.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgment
&lt;/h2&gt;

&lt;p&gt;Early on in Paper’s life, an artist and a fan of Paper reached out to offer his services.&lt;/p&gt;

&lt;p&gt;The most prominent result of our collaboration is Paper’s palette of accent colors. Especially the signature &lt;em&gt;Sepia&lt;/em&gt; color that was inspired by &lt;a href="https://en.wikipedia.org/wiki/Sepia_(color)"&gt;sepia ink&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hats off to &lt;a href="https://penwave.com/?ref=papereditor.app/dev"&gt;Ben Marder&lt;/a&gt; for his help. 🎩&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ojWxRjqB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/accents.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ojWxRjqB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://papereditor.app/dev-article/accents.png" alt="A screenshot of the iPhone app accent color picker." width="750" height="998"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we’ve reached the end of the article, something I simply cannot leave unmentioned is that at one point Paper was &lt;a href="https://twitter.com/papereditorapp/status/1479415502132940804"&gt;printed in a physical magazine&lt;/a&gt;. For a software developer, it is a pretty surreal experience to have your digital code manifest in real life like that. 🤤&lt;/p&gt;

</description>
      <category>design</category>
      <category>ui</category>
      <category>mobile</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
