NSTextView (Plain Text) and the Pasteboard: PasteboardType.string Is Not Handled

For a plain text (not rich text/RTF) NSTextView, I found that:

  • writablePasteboardTypes contains only "NSStringPboardType" by default.
  • readablePasteboardTypes contains a lot of types, but only "NSStringPboardType" for plain text copying.-
    An NSPasteboard understands both "NSStringPboardType" and the NSPasteboard.PasteboardType.string value.

Possible Fixes

Since NSTextView doesn’t understand the NSPasteboard.PasteboardType.string pasteboard type for reading or writing, I tried two approaches to handle plain text input (pasting) and output (cut/copy):

  1. Add a backport for the deprecated raw value, "NSStringPboardType";
  2. Patch NSPasteboard.PasteboardType.string into the list of readable/writable types.

The backport seems to work, but extending supported types sounds like more robust solution.

Expand readable and writable pasteboard types

To give the “new” (since masOS 10.6) pasteboard type higher precedence, prepend it to the default types:

class MyTextView: NSTextView {
    override var readablePasteboardTypes: [NSPasteboard.PasteboardType] {
        return [NSPasteboard.PasteboardType.string]
            + super.readablePasteboardTypes
    }

    override var writablePasteboardTypes: [NSPasteboard.PasteboardType] {
        return [NSPasteboard.PasteboardType.string]
            + super.writablePasteboardTypes
    }
}

In my subclass, I’m not re-computing this on the fly, but cache the resulting array.

When you add a breakpoint to NSTextView’s writeSelection(to:type:) and inspect the type that has been picked, this approach will set the type parameter to NSPasteboard.PasteboardType.string if applicable.

Fair warning: I haven’t tested if forwarding to super works for all imaginable cases. My personal intention is to handle the .string case separately and replace the system default behavior. So I’m fine with that caveat.

Backport of "NSStringPboardType"

The Objective-C constant is not exposed to Swift, so you need to initialize a new NSPasteboard.PasteboardType:

override func writeSelection(
    to pasteboard: NSPasteboard,
    type: NSPasteboard.PasteboardType
) -> Bool {
    switch type {
    case .string,  // Actual raw value of "public.utf8-plain-text"
         .init(rawValue: "NSStringPboardType"):
        // Do something with the string
    default:
        return super.writeSelection(to: pasteboard, type: type)
    }
}

I’m actually using a static constant to avoid typos in the string wherever needed, and to add a doc comment:

extension NSPasteboard.PasteboardType {
    /// Backport of the value since Objective-C days that is used when copying from a `NSTextView`.
    ///
    /// According to the header files, the Objective-C `NSStringPboardType` constant should've been replaced with `NSPasteboardTypeString` since macOS 10.6, but the `rawValue` of the .string case accessible to Swift is "public.utf8-plain-text". These don't match.
    static let _stringLegacy: NSPasteboard.PasteboardType = .init(rawValue: "NSStringPboardType")
}

In fact, I’m combining both approaches, so I’m prepending the .string pasteboard type to the arrays of acceptable types, and I use .string and ._stringLegacy in my switch-case statements. I don’t understand the edge cases here, so I’m rather safe than sorry.

Observations

These are more or less the raw observations I made. It’s my lab notes so you can compare with your findings, and understand how I come to my conclusions.

NSStringPboardType is not bridged to Swift

Objective-C’s NSStringPboardType was deprecated in macOS 10.14, but the replacement NSPasteboardTypeString is available since macOS 10.6. – Keep this in mind why some collections contain both (for legacy compatibility reasons most likely).

NSPasteboardTypeString is bridged to Swift via NSPasteboard.PasteboardType.string. Its rawValue is public.utf8-plain-text, which is different fromNSStringPboardType.

APPKIT_EXTERN NSPasteboardType const NSPasteboardTypeString	 		API_AVAILABLE(macos(10.6)); // Replaces NSStringPboardType
// ...
APPKIT_EXTERN NSPasteboardType NSStringPboardType API_DEPRECATED_WITH_REPLACEMENT("NSPasteboardTypeString", macos(10.0,10.14));

NSPasteboard comes with multiple string representations

Inspecting a pasteboard when ⌘V pasting plain text reveals that both the new .string type and the old NSStringPboardType is accessible, so you can use pasteboard.string(forType: .string) without problems.

# At breakpoint in readSelection(from:type:)
(lldb) po pasteboard.types
▿ Optional<Array<NSPasteboardType>>
  ▿ some : 2 elements
    ▿ 0 : NSPasteboardType
      - _rawValue : public.utf8-plain-text
    ▿ 1 : NSPasteboardType
      - _rawValue : NSStringPboardType

That’s the only place where this works as expected, since the text view itself doesn’t understand the .string value.

NSTextView supports a lot of types, but not NSPasteboardType.PasteboardType.string

Pasting plain text goes through the legacy API of NSStringPboardType. That’s the only writable (cut/copy) type, and it’s not equal to NSPasteboardType.PasteboardType.string:

(lldb) po myTextView.writablePasteboardTypes
▿ 1 element
  ▿ 0 : NSPasteboardType
    - _rawValue : NSStringPboardType

Reading is more versatile, but also only lists the legacy type, not NSPasteboardType.PasteboardType.string:

(lldb) po myTextView.readablePasteboardTypes
▿ 9 elements
  ▿ 0 : NSPasteboardType
    - _rawValue : NSStringPboardType
  ▿ 1 : NSPasteboardType
    - _rawValue : NeXT Rich Text Format v1.0 pasteboard type
  ▿ 2 : NSPasteboardType
    - _rawValue : NeXT RTFD pasteboard type
  ▿ 3 : NSPasteboardType
    - _rawValue : Apple HTML pasteboard type
  ▿ 4 : NSPasteboardType
    - _rawValue : WebURLsWithTitlesPboardType
  ▿ 5 : NSPasteboardType
    - _rawValue : public.url
  ▿ 6 : NSPasteboardType
    - _rawValue : Apple URL pasteboard type
  ▿ 7 : NSPasteboardType
    - _rawValue : NSFilenamesPboardType
  ▿ 8 : NSPasteboardType
    - _rawValue : NeXT ruler pasteboard type

Overview of Attribute Fixing in NSTextStorage

NSTextStorage provides attribute fixing of user-entered text, via NSMutableAttributedString.fixAttributes(in:).

Attribute fixing…

  • … applies font fallbacks to special characters not included in the text font, e.g. “ZapDingbats” for some Unicode symbols, or “AppleColorEmoji” for Emoji display
    • “assigns default fonts to characters with illegal fonts for their scripts and otherwise corrects font attribute assignments”1
  • … makes all characters in a paragraph inherit the paragraph style
    • “assigns the first paragraph style attribute value in each paragraph to all characters of the paragraph”1
  • … removes attachment attributes for any character in range that is not NSTextAttachment.character1

NSMutableAttributedString.fixAttributes(in:) always work on full paragraph ranges. It extends the passed-in range as needed “to cover the last paragraph partially contained”1.

Attribute fixing is automatically applied from NSTextStorage.processEditing() via invalidateAttributes(in:) “when the text storage changes”.2

The attribute fixing mechanism can be applied eagerly or lazily:

  1. Eager attribute fixing
    • Requires subclassing: fixesAttributesLazily has to return false. The default, when you initialize a standard NSTextStorage (i.e. the concrete hidden subclass), is true. When you subclass, the default is false.
    • Makes invalidateAttributes(in:) call NSMutableAttributedString.fixAttributes(in:) directly (eagerly).
  2. Lazy attribute fixing
    • Default behavior of the system default concrete NSTextStorage subclass.
    • Makes invalidateAttributes(in:) enqueue the range for later instead of processing it immediately.
    • Internally requires ensureAttributesAreFixed(in:) calls before any attribute access. This in turn processes enqueued invalidated ranges.
    • Internally requires subclasses to “avoid directly calling fixAttributes(in:) or else bracket such calls with beginEditing() and endEditing() messages.”1
  1. Apple API docs: NSMutableAttributedString.fixAttributes(in:), https://developer.apple.com/documentation/foundation/nsmutableattributedstring/1533823-fixattributes, accessed 2022-09-21  2 3 4 5

  2. Apple API docs: NSTextStorage.invalidateAttributes(in:), https://developer.apple.com/documentation/uikit/nstextstorage/1534025-invalidateattributes, accessed 2022-09-21 

'Black Box' Unifies Software Modeling in Modules

The metaphor of the “black box” is very common in programming.

It is so general that it’s nearly meaningless; but it summarizes a lot of specialized principles nicely and shows a unifying principle.

A black box is characterized by:

  • a boundary
  • inputs
  • outputs
A black box has inputs, outputs and ... something happens inside

It can be applied to “modules” on all levels of abstraction.

The I/O part is obvious when you write code and design objects or functions. With functions, the parameter list and return value says it on the tin.

Quite a few people stop there.

But the boundary part is even more important: it determines what’s inside, and what’s out.

Why does a parameter list have these and not other parameters?

Is there any logic to the choice? (Indicator of high cohesion)

Is the inside something you can summarize without having to pry open the box?

Example of High Cohesion and Low Coupling with Presenter, View, and View Model

Here’s a short definition of the two terms:

Cohesion is about how well elements within a module belong together and serve a common purpose. Coupling is about how much one module depends or interacts with other modules. Thus, cohesion is an intra-module concern whereas coupling cuts across modules [aka inter-module].

(Devopedia)

Example for highly cohesive module:

  1. A presenter receives some data, passing it
  2. to its formatter (service) to assemble
  3. a view model, which is then displayed
  4. in a view component.

These four objects in bold type are part of a module; not in the Swift sense where module == library, but in the sense that they form a whole. A package that logically belongs together.

Now there can be many such modules/folders/packages/Xcode groups that pass data from one place to another. But there’s more path-ways inside each module than between them. (It being an “intra-module concept” means it pertains the insides of a module and relationship of its pieces, instead of the relationship of modules themselves.)

Good modularization: high cohesion inside the module, and low coupling to its components, just one 'intrusion' via the Presenter

In the diagram above, you see how another module is coupled to the example module by accessing the presenter. But that’s just one breach of the module’s boundary.

If you move the pieces around, breaking up the task- and domain-specific modules into mere technical modules, you may end up with one big directory of views, one of view models, one of utils (with the formatter inside), one of presenters.

Bad modularization: the components are grouped by their technical aspects (like M-V-C), and thus almost all the arrows go across boundaries.

Then there’ll be a lot of back and forth across these modules’ boundaries.

This effectively makes the groups useless, because they don’t convey any meaning. There’s no intent being expressed, no design of a component with its pieces.

This grouping creates an order similar to alphabetical sorting. There’s some algorithmis sense in it, but it doesn’t help understand the software.

That’s what people talk about when they say that high coupling is bad. In a case like this, it’s unavoidable, and there’s no easy way out if you stick to the setup. So you cannot “solve” high coupling like you solve an equation; you need to break the coupled pieces up and rearrange everything until a comprehensible structure emerges from your design. (Emphasis on “comprehensible” – it’s for humans –, and “design” – it’s an intentional act, not a brainless activity.)

How to Fix When Some Text Changes Don't Come with Automatic Undo?

When you work with NSTextView and happen to use insertText(_:) to programmatically insert text, you get an undoable action for free.

This might give the impression you get undo/redo functionality for free.

Eventually, you’ll notice how other changes don’t have an affordance in the “Edit” menu. While it’s possible to get “Undo Typing” and “Undo Set Color” from some function calls, it’s not possible to get “Undo Change Text Attributes” when you use NSTextStorage.addAttributes(_:range:).

That doesn’t mean that addAttributes is broken or you’re holding it wrong – it’s just that some text view methods are exactly what is being used for user-interactive changes, and these come with auto-undo, while actual programmatic API does not.

If you look at the docs for insertText(_:), it says:

This method is the entry point for inserting text typed by the user and is generally not suitable for other purposes. Programmatic modification of the text is best done by operating on the text storage directly. Because this method pertains to the actions of the user, the text view must be editable for the insertion to work.

So this is the same entry point for user-interactive changes, and it adheres to the rules of isEditable. That’s not a good fit for programmatic changes.

But if you call the programmatic API of NSTextStorage, which is just an NSMutableAttributedString, you end up with replaceCharacters(in:with:) and don’t get undo/redo for free anymore.

The good news is that adding undo/redo for programmatic API changes is also very simple.

The default UndoManager is set to group all registered blocks into 1 undoable action for each pass of the RunLoop. That’s the main app’s while-loop that polls for events and continuously draws the app’s contents for you, basically. You are on the same “run loop pass” as long as you don’t enqueue an action asynchronously on a dispatch queue, background thread, or via RunLoop.perform(_:).

As a consequence, when you call UndoManager.registerUndo(withTarget:handler:) in 5 different view controllers all reacting to the same notification, these 5 registered undo blocks will be coalesced into 1 undoable action because they’re in the same group on the same run loop pass.

Bottom-line: If you get undo/redo for free from calling some method on NSTextView, chances are these were meant to be user-interactive and not programmatic API, so you should consider using a different way to achieve your goal via the underlying NSTextStorage. That means you need to register the inverse action with the UndoManager’s undo stack. This can be done anytime and will automatically be grouped into 1 user-undoable action, so you need to do less than you might have feared.

Update 2022-09-14: Matt Massicotte (@mattie) pointed out:

Interacting with NSTextStorage directly will not correctly take into account text selection. How much of an issue this is depends, but definitely something to be aware of.

That’s true, you have to do everything manually, including selection restoration when undoing. Maybe a good post for another day.

Reactive Code is Sequentially Cohesive

Reactive, declarative code is sequentially cohesive: you have a sequence of events and reactions to events, and it’s pieces are tied together real close.

The processing chain itself is then a function or feature of the app. The chain of operators is a thing itself; the step-by-step transformation is then reified into a sequence in one place: it has a beginning, an end, a sequential order of steps.

That makes the encapsulation of the sequence is very clear.

Compared to some object-oriented factorings of code, reactive code is often very good at showing such sequences in one place. It’s “data-driven” inasmuch as you transform data from one format to another, and eventually produce a result or side effect.

With traditional object-oriented factoring techniques, you might be inclined to split up multiple transformational steps into isolated objects, then either couple them sequentially so information flows from object to object by virtue of delegation from A→B→C→D, which is hard to follow and see at a glance, or have a “master sequence” call each object in succession and pipe the results from one to another.

Back when I took note of this in 2016, Rx was all the rage and looked somewhat different. To preserve the historic example that even predates Swift.URL, here’s the original live search text field by Marin Todorov that shows the sequential nature of steps:

query.rx_text

  .filter { string in
      return string.characters.count > 3
  }

  // Coalesce rapid-firing of events
  .debounce(0.51, scheduler: MainScheduler.instance)

  .map { string in
      let apiURL = NSURL(string: "https://api.github.com/q?=" + string)!
      return NSURLRequest(URL: apiURL)
  }

  .flatMapLatest { request in
      return NSURLSession.sharedSession().rx_data(request)
  }

  .map { data > Array<AnyObject> in
      let json = try NSJSONSerialization.JSONObjectWithData(data,
          options: [])
      return json as! Array<AnyObject>
  }

  .map { object in
      return Repo(object: object)
  }

  .bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))

A traditional approach might look like this, for example:

@IBOutlet var tableView: ReposTableView?
lazy var debouncer = Debouncer(interval: 0.51, delegate: self)
lazy var gitHubAPI = GitHubAPI()
lazy var jsonDecoder = JSONDecoder()

// Entry point
func handleTextChange(string: String) {
    guard string.count > 3 else { return }
    debouncer.debounce(string)
}

// DebouncerDelegate callback
func debouncerDidDebounce(_ debouncer: Debouncer, string: String) {
    gitHubAPI.repos(forQuery: string) { [weak self] result in
        switch result {
        case .success(let repos):
            self?.tableView.repos = repos
        case .failure(_):
            errorHandlingIsAnExerciseForTheReader()
    }
}

You see how employing different objects here requires some back and forth already, to hand things off from the caller to the Debouncer, then get back a debounced result at some later time via a callback: It’s already 2 functions you need to look at, 2 places that are part of a sequence, but the middle portion is invisible here. The part that glues handleTextChange to debouncerDidDebounce is not apparent. That’s part of the pain of splitting code into multiple functions or event objects.

You might think that it’d help to make the debouncer block-based – but since the debouncing is stateful, it makes more sense to have one shared callback, too. So you’d pass the block in the Debouncer.init, not at the call site in debounce, and still end up with 2 places you need to look at.

It actually is quite nice, though, that GitHubAPI encapsulates the processing of search strings to URL queries to Data results to JSON and then to Repo objects. So there’s a sub-sequence hidden inside the repos(forQuery:) call. (That’s totally possible in the reactive code as well, of course, but would obfuscate the nature of the example Marin provided.)

Delete to Beginning of Line in Emacs to Rewrite Code

Ever since Brett Terpstra posted about ⌘← to jump to the first character of the line back in 2014, I’ve used behavior like this in Xcode. Nowadays, ⌘⌫ automatically deletes up to the beginning of the line, stopping at whitespace. That’s quite handy to rewrite the current line.

The default key bindings in Emacs were always a bit too eager for my taste, because Control-based deletion would delete the whole line, and Meta-based deletion would delete to to the beginning of the sentence.

To make this a bit more “sane” for my habits, here’s a function that kills to the beginning of the actual line in programming modes, and to the beginning of the visual line in other modes – for me, that’s mostly prose editing like this Markdown document, but we’ll see if I can come up with a better predicate.

(defun ct/kill-to-beginning-of-line-dwim ()
  "Kill from point to beginning of line.

In `prog-mode', delete up to beginning of actual, not visual
line, stopping at whitespace. Repeat to delete whitespace. In
other modes, e.g. when editing prose, delete to beginning of
visual line only."
  (interactive)
  (let ((current-point (point)))
      (if (not (derived-mode-p 'prog-mode))
          ;; In prose editing, kill to beginning of (visual) line.
          (if visual-line-mode
              (kill-visual-line 0)
            (kill-line 0))
        ;; When there's whitespace at the beginning of the line, go to
        ;; before the first non-whitespace char.
        (beginning-of-line)
        (when (search-forward-regexp (rx (not space)) (point-at-eol) t)
          ;; `search-forward' puts point after the find, i.e. first
          ;; non-whitespace char. Step back to capture it, too.
          (backward-char))
        (kill-region (point) current-point))))

I bound my Command key to “Super”, so this is my shortcut of choice now:

(global-set-key (kbd "s-<backspace>") #'ct/kill-to-beginning-of-line-dwim)

10 Year Indieversary AMA Postmortem

Thanks everyone for joining the AMA yesterday! Was a fun experience :)

We talked about a couple of technical and historic things. Here’s an outline of the pieces:

  • My first App Store success
    • When I released Calendar Paste v1 in 2012, I shaved my head and got a mohawk to celebrate the sales.
    • Calendar Paste was submitted on 2012-11-25 and got approved 2012-12-14. Reviews times back then were the worst!
    • Back then I posted about the time I logged to work on this v1:

      The first, very successful two weeks of sale having passed, this adds up to a whopping $1.03 hourly rate! I sold the app 181 times […].

    • 10 years later, iOS Calendar.app still doesn’t support copy and pasting.
  • My product release dates (major versions only)
    • 2018-03-15: The Archive v1
    • 2016-09-27: TableFlip v1
    • 2015-12-11: Make Money Outside the Mac App Store (ebook)
    • 2015-11-02: Minimal Writing on the Mac (ebook)
    • 2015-09-16: Calendar Paste v3
    • 2015-08-04: Exploring Mac App Development Strategies (ebook)
    • 2014-07-04: WordCounter v1
    • 2014-05-20: Tapping Test app
    • 2014-03-03: Calendar Paste v2
    • 2012-11-24: Calendar Paste v1
    • 2010-10-05: First post on christiantietze.de that’s still available
  • My take on subscription as a business model
    • As a developer, perpetual income is super nice.
    • Business-wise, it makes sense to bill regularly for a service you provide: maintenance and updates.
      • Depending on the subscription model, this turns your app into a SaaS.
      • This raises (well-known) questions about ownership of software, and licenses.
    • As a user, I prefer the Sketch pricing model of “pay for 1 year of updates”, aka “non-renewable subscriptions” (thanks for the term, Marin!).
    • Panic’s text-editor Nova works in a similar way: “ Cancel anytime and keep what you have, forever. Easily resume later.”
      • $99 for the first year, $49 for each following year.
      • They promise 1 year of updates and manage expectations really well.
      • When their main dev needs a break, your subscription is extended for you so you don’t miss out. (→ Panic’s Newsletter
        • Poor Logan seems to be the sole dev, amazing!
        • This also highlights how much pressure this could build up.
    • Do you want to worry about people skipping every odd year? When you re-subscribe, you’ll get the latest version, so waiting out 1 year of updates can save you money. Is that unfair?
      • I personally don’t think so. They do miss out for a whole year, after all.
      • It’s a bit of a hassle to “game” this, and you don’t get much in return as a user, so as a dev, I would not worry about this. Other users likely won’t care, either.
    • Derek Sivers sells his content to you, no matter the format.
      • Purchase the hardcover version of the book, and you get the ebook as well.
      • If you have access to the ebook, you also get access to audio and video versions of the same content.
      • That sounds fair to me, in principle. But as a content creator, I don’t know if this would be a viable business model.
      • Keep in mind: Derek doesn’t need the money. He’s financially independent as far as I can tell from his writing and podcasts.
      • Derek sells you the content for $15. That’s not a so-called “sustainable” price for a nice product when you have to spend months writing, then more months recording audio and cutting everything, then even more resources on a video format. Business-wise, you’d need a $15 product like this to sell a metric crapton of copies.
      • If the average developer/content creator wants to replicate the approach and needs charge more for ebook + audio + video to make the thing work well, you end in a B2B or premium segment, selling a more expensive bundle, say at $200, or maybe discounted to $99 even when people purchase everything at once. $15 is crazy low unless you have an abundance mindset and reaaaaaally want to get the content out into the world (offsetting the production cost as “marketing”).
    • We also explored micro-transactions for features when Oliver pointed out this doesn’t seem to be done. Token economy in apps. Didn’t resonate much :) Maybe that’s why we don’t see that outside of gaming.
    • In-app purchases to buy new features are an alternative.
      • I think beorg does this pretty well.
        • You can buy various “advanced” features, and I noticed how these were added over time.
        • Beorg also has a tip jar. That’s where their income from me stems from. I’m tipping a couple of Euros every now and again when an update does something cool, or the release notes are surprisingly long and detailed.

Replacing zoom-window with winner-mode to Temporarily Change Window Splits in Emacs

On Reddit, there was a recent thread called “Your dependency on external packages reduce with experience”.

I have been using zoom-window in the past to temporarily “unsplit” buffers. So this:

┌───┐╔════════════╗┌─────┐
│   │║            ║│     │
│ A │║      C     ║│  B  │
│   │║            ║│     │
│   │║ focus,     ║│     │
│   │║   center   ║│     │
└───┘╚════════════╝└─────┘

Turns into this:

╔════════════════════════╗
║                        ║
║      C                 ║
║                        ║
║ focus,                 ║
║   center               ║
╚════════════════════════╝

The clou with “zooming” is that this is just a temporary override. In Xcode, you have a similar functionality to collapse split editors temporarily. That’s useful to focus on code, esp. long lines, or when the display is small.

There’s a built-in Emacs mode, winner-mode, that replaced this functionality for me. winner-mode allows undoing and redoing changes to the window layout.

This effectively is a functionally equivalent approach: instead of using a package that unsplits temporarily, I can just use the regular shortcut to unsplit windows, and then undo the change later.

Using this more general package enables different uses. If I split a window three times, I can undo the last split and keep two. It’s more flexible in that regard. It also saves me from accidental changes to carefully crafted layouts.

So in the spirit of the Reddit discussion, I noticed that I, too, learned to use built-in functionality instead of relying on an external package that I thought I needed.

Don’t get me wrong: zoom-window is great and works reliably! I looked for this to replicate the behavior of Xcode; now I know my way around Emacs a bit better, in part thanks to Mickey Petersen’s “Demystifying Emacs’s Window Manager”, so I am now able to wield a more complex tool.


→ Blog Archive