Orphans! Designers hate them!

Well orphans in text anyways…

What is an orphan? If you’ve ever seen one word at the bottom of a paragraph in a body of text, that is an orphan.

The last word of this canned paragraph will be an annoying
orphan.

Wow! So annoying! I’ve been in many meetings where a demo has gone smoothly but the last thing that stands out that needs fixing is the orphan in a bit of text that needs to be cleaned up. With good reason too! They look sloppy and can look jarring.

How do we set up an orphan? Here’s some code in Swift made for iOS 13…

let orphanTextView = UITextView()
orphanTextView.frame = CGRect(x: 0, y: 96, width: 200, height: 64)
orphanTextView.font = font
orphanTextView.text = NSLocalizedString("Hello World! I am an annoying orphan.", comment: "A string describing an orphan.")
orphanTextView.textColor = .black
orphanTextView.backgroundColor = .systemOrange

And here’s how it looks…

A quick fix is to just add a newline like so:

let firstOrphanFix = UITextView()
firstOrphanFix.frame = CGRect(x: 0, y: 192, width: 200, height: 64)
// firstOrphanFix.frame = CGRect(x: 0, y: 192, width: 300, height: 64) // Breaks at 300
firstOrphanFix.font = font
firstOrphanFix.text = NSLocalizedString("Hello World! I am an\nannoying orphan.", comment: "A string describing an orphan.")
firstOrphanFix.textColor = .black
firstOrphanFix.backgroundColor = .systemYellow

Hey that looks great you might say!

Unfortunately it breaks once the size of the text view changes:

This approach can work in a pinch but there’s a better way. Let’s try a non-breaking space…

let secondOrphanFix = UITextView()
secondOrphanFix.frame = CGRect(x: 0, y: 288, width: 200, height: 64)
// secondOrphanFix.frame = CGRect(x: 0, y: 288, width: 300, height: 64)
secondOrphanFix.font = font
secondOrphanFix.text = NSLocalizedString("Hello World! I am an annoying\u{00a0}orphan.", comment: "A string describing an orphan.")
secondOrphanFix.textColor = .black
secondOrphanFix.backgroundColor = .systemGreen

That looks identical to our newline approach, which is good! What happens if we expand our view though?

let secondOrphanFix = UITextView()
// secondOrphanFix.frame = CGRect(x: 0, y: 288, width: 200, height: 64)
secondOrphanFix.frame = CGRect(x: 0, y: 288, width: 300, height: 64)
secondOrphanFix.font = font
secondOrphanFix.text = NSLocalizedString("Hello World! I am an annoying\u{00a0}orphan.", comment: "A string describing an orphan.")
secondOrphanFix.textColor = .black
secondOrphanFix.backgroundColor = .systemGreen

Hey! That looks a lot better! Seems like we’ve found a somewhat general solution. This should look nice on any iOS (and iPad OS) device. This has some cons though. This approach would be really, really annoying and hard to maintain across your entire app in all your different text views. It makes your text strings a bit more difficult to read for the developer, as well as being hard for your translator to know why there is that non-breaking space. All the strings that are translated would also have to have any non-breaking space added to the end of those new strings. Any remote content that is pulled over an API and displayed in a UITextView also wouldn’t have any orphans fixed, as we’ve only found a hard-coded string fix.

Can we pull this into a computed property that we can apply to any string?

extension String {

  // Returns a string that has the last space replaced with a non-breaking
  // space. norph is short for no orphans
  var norph: String {
    var rVal = self

    guard let lastSpaceIndex = rVal.lastIndex(where: { $0 == " " } ) else {
      return self
    }

    rVal.replaceSubrange(lastSpaceIndex...lastSpaceIndex, with: "\u{00a0}")

    return rVal
  }
}

let thirdOrphanFix = UITextView()
thirdOrphanFix.frame = CGRect(x: 0, y: 384, width: 200, height: 64)
thirdOrphanFix.font = font
thirdOrphanFix.text = NSLocalizedString("Hello World! I am an annoying orphan.", comment: "A string describing an orphan.").norph
thirdOrphanFix.textColor = .black
thirdOrphanFix.backgroundColor = .systemBlue

Still looking pretty good!

We can even pull this into a subclass, so we don’t need to call `norph` on every string.

class NorphTextView: UITextView {
    override var text: String! {
        set {
            super.text = newValue.norph
        }
        get {
            return super.text
        }
    }
}

let fourthOrphanFix = NorphTextView()
fourthOrphanFix.frame = CGRect(x: 0, y: 480, width: 200, height: 64)
fourthOrphanFix.font = font
fourthOrphanFix.text = NSLocalizedString("Hello World! I am an annoying orphan.", comment: "A string describing an orphan.")
fourthOrphanFix.textColor = .black
fourthOrphanFix.backgroundColor = .systemIndigo

You might think we are done here. There’s one last bit that isn’t technical though. You’ve still got a game of whack-a-mole afoot. How are you and your designer on the same page when knowing when and where to apply this functionality to a text view? You could go through your app string-by-string, but that is tedious, prone to error and hard to maintain, either littering your code with `norph`s everywhere, or having every text view you use have to be a subclass of `NorphTextView`.

This is where a pattern library comes in. The cliff notes definition of a pattern library is a collection of common UI elements used within your app. If within that collection there was a `PrimaryTextView` used within your app, what would that look like?

class PrimaryTextView: UITextView {
    override var text: String! {
        set {
            super.text = newValue.norph
        }
        get {
            return super.text
        }
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)

        font = UIFont.systemFont(ofSize: 12)
        textColor = .black
        backgroundColor = .systemPurple
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

let finalOrphanFix = PrimaryTextView()
finalOrphanFix.frame = CGRect(x: 0, y: 576, width: 200, height: 64)
finalOrphanFix.text = NSLocalizedString("Hello World! I am an annoying orphan.", comment: "A string describing an orphan.")

You and your designer know what a `PrimaryTextView` is, and know when they see one within your app. There is no question now if you see a `PrimaryTextView` on whatever sized device if it has its orphans handled or not.

This could be overkill for your use-case though… as of iOS 11 you can just use a UILabel. It has some nice behaviour around handling orphans.

let orphanLabel = UILabel()
orphanLabel.frame = CGRect(x: 0, y: 0, width: 200, height: 64)
orphanLabel.numberOfLines = 0
orphanLabel.font = font
orphanLabel.text = NSLocalizedString("Hello World! I am an annoying orphan.", comment: "A string describing an orphan.")
orphanLabel.textColor = .black
orphanLabel.backgroundColor = .systemRed

You lose the nice padding you get with a UITextView though.

Hope somebody found this blog post useful!