Generating iOS App Icons via ImageMagick

Oftentimes you find yourself having to generate multiple sized icons for your iOS app. There are applications on the app store that help you do this, as well as websites. You are at the mercy of them having been updated on time to support new versions of iOS / iPad OS / etc. Wouldn’t it be nice if you could have this as a scheme/target in your app that you could update yourself as needed?

Let me show you how!

We created a new iOS/iPad OS (On macOS Big Sur 11.6, Xcode 13.0) project called OneCoolApp. Let’s add a new aggregate target:

Change the new target name to GenerateAppIcons. Now let’s add a new Run Script phase:

macOS uses zsh by default and is included on recent macos installations. We use a feature of it called an Associative Arrays. Make sure to change the shell like so:

Now for the script content. Copy paste this in:

PATH="/opt/local/bin:/opt/local/sbin:$PATH" # Needed for my personal dev machine
ROOT_IMAGE_PATH="$SOURCE_ROOT/OneCoolApp/Assets.xcassets/AppIcon.appiconset/AppStoreIcon.png"

APP_ICON_PATH="$SOURCE_ROOT/OneCoolApp/Assets.xcassets/AppIcon.appiconset/"

declare -A APP_ICON_SIZING # That aforementioned associative array

APP_ICON_SIZING[iphone-20pt@2x]="40x40"
APP_ICON_SIZING[iphone-20pt@3x]="60x60"

APP_ICON_SIZING[iphone-29pt@2x]="58x58"
APP_ICON_SIZING[iphone-29pt@3x]="87x87"

APP_ICON_SIZING[iphone-40pt@2x]="80x80"
APP_ICON_SIZING[iphone-40pt@3x]="120x120"

APP_ICON_SIZING[iphone-60pt@2x]="120x120"
APP_ICON_SIZING[iphone-60pt@3x]="180x180"

APP_ICON_SIZING[ipad-20pt@1x]="20x20"
APP_ICON_SIZING[ipad-20pt@2x]="40x40"

APP_ICON_SIZING[ipad-29pt@1x]="29x29"
APP_ICON_SIZING[ipad-29pt@2x]="58x58"

APP_ICON_SIZING[ipad-40pt@1x]="40x40"
APP_ICON_SIZING[ipad-40pt@2x]="80x80"

APP_ICON_SIZING[ipad-76pt@1x]="76x76"
APP_ICON_SIZING[ipad-76pt@2x]="152x152"

APP_ICON_SIZING[ipad-83.5pt@2x]="167x167"

for name size in ${(kv)APP_ICON_SIZING};
do
    convert $ROOT_IMAGE_PATH -resize $size\! $APP_ICON_PATH/Icon-$name.png
done

First notice that ROOT_IMAGE_PATH points to the largest image in our asset library. Let’s add an image in that spot before we run this scheme/target. Name it AppStoreIcon.png. Feel free to use the following image:

After adding it should look like this in your project:

One more thing before we run the scheme/target! We need to make sure that ImageMagick is installed. I used MacPorts (via $ sudo port install ImageMagick) but you may install it via other means. That’s what the convert command uses in the shell script.

Build the scheme/target having selected it like so:

Your assets should now look like this!

Drag and drop the unassigned icons to their proper places in the assets library so it looks like so:

You are good to go! If the app store icon ever changes you just need to run this scheme/target and it will regenerate your assets for you.

Let’s try that out actually. Here is a new (hastily designed moon/banana/boomerang) app store icon for us to use:

Your assets library should look like this after adding it:

Build the GenerateAppIcons scheme/target again… amazing. Your suns have turned into moons/bananas/boomerangs:

Pretty cool! This saves time when working with icons that are changing, can be extended to include app extensions as well (such as iMessage extensions) watch apps, etc… when new devices are introduced you just have to update your shell script to output those new sizes and add them to your assets library, rather than waiting for third party tools to update.

There are some downsides… Oftentimes the app icons don’t scale nicely from 1024×1024 down to 20×20. You’ll have to get your designer to design icons that small that highlight the fine features from the large icon that you want to communicate. Some extensions have odd sizes as well, such as some icons needed in an iMessage extension, like 74×55.

For a large subset of use cases though this technique can save a lot of time.

Matrix on Linux

Expanding on my previous blog post, I wanted to get those cool characters displaying in my ncurses from DOS port. Namely, these characters:

日ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ012345789Z:・.\"=*+-<>¦|╌çリク

According to a well informed stack exchange post at: https://scifi.stackexchange.com/a/182823 those are the characters you’d see on the matrix screen. They encode the 👱‍♀️, 👨🏽‍⚕️and 👩‍🦰.

To display characters wider than 8 bits in ncurses you need a… wait for it… wide ncurses, namely ncursesw.

I tried doing this on macOS, but I couldn’t figure out how to get an installed version of ncursesw to play nicely with the already bundled version.

Luckily Swift runs on Ubuntu! So we installed Ubuntu 18.04, installed some dependencies ($ sudo apt install libncurses5-dev and $ sudo apt install libncursesw5-dev) and we were off to the races.

I made a nice Swift package for this at https://github.com/csdiweb/CNCURSESW. I used that as a dependency like so:

// swift-tools-version:5.2
import PackageDescription

let package = Package(
  name: "matrixTwoDotOh",
  products: [
    .executable(name: "matrixTwoDotOh", targets: ["matrixTwoDotOh"])
  ],
  dependencies: [
    .package(url:  "https://github.com/csdiweb/CNCURSESW", from: "1.0.0")
  ],
  targets: [
    .target(
      name: "matrixTwoDotOh",
      dependencies: [])
  ]
)

Now we can just do a import CNCURSESW_LINUX in our code. Here is our updated source, now running on Ubuntu:

//
//  main.swift
//  matrixTwoDotOh
//
//  Created by Daniel Drzimotta on 2020-05-07.
//  Copyright © 2020 Daniel Drzimotta. All rights reserved.
//

import Glibc
import CNCURSESW_LINUX

struct NCursesPoint {
    var x: Int32
    var y: Int32
}

struct NCursesSize {
    var width: Int32
    var height: Int32
}

protocol MatrixString {
    var position: NCursesPoint { get }
    var length: Int32 { get }
    var displayCharacter: Character { get }

    mutating func update()
}

let initialLengthFunc = { Int32.random(in: 0..<10) + 4 }

let characterSet = "日ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ012345789Z:・.\"=*+-<>¦|╌çリク"

// With help from: https://stackoverflow.com/questions/49451164/convert-swift-string-to-wchar-t
extension String {
    /// Calls the given closure with a pointer to the contents of the string,
    /// represented as a null-terminated wchar_t array.
    func withWideChars<Result>(_ body: (UnsafePointer<wchar_t>) -> Result) -> Result {
        let u32 = self.unicodeScalars.map { wchar_t(bitPattern: $0.value) } + [0]
        return u32.withUnsafeBufferPointer { body($0.baseAddress!) }
    }
}

extension MatrixString {
    func draw() {
        var special: cchar_t = cchar_t()

        String(displayCharacter).withWideChars { asWideChars in
            setcchar(&special, asWideChars, WA_NORMAL, 1, nil)
            mvadd_wch(position.y, position.x, &special)
        }
    }

    static var initialLength: Int32 {
        Int32.random(in: 0..<10) + 4
    }
}

struct MatrixCharacterString: MatrixString {
    var position: NCursesPoint
    var maxPosition: NCursesSize

    var length: Int32
    var displayCharacter: Character = Character(" ")

    init(maxPosition: NCursesSize) {
        self.position = NCursesPoint(x: Int32.random(in: 0..<maxPosition.width), y: Int32.random(in: 0..<maxPosition.height) - 1)
        self.maxPosition = maxPosition

        self.length = initialLengthFunc()
    }

    mutating func update() {
        displayCharacter = characterSet.randomElement() ?? Character(" ")
        length -= 1
        position.y += 1

        if length < 0 {
            self = MatrixCharacterString(maxPosition: maxPosition)
        }
    }
}

struct MatrixClearString: MatrixString {
    var position: NCursesPoint
    var maxPosition: NCursesSize

    var length: Int32
    var displayCharacter: Character = Character(" ")

    init(maxPosition: NCursesSize) {
        self.position = NCursesPoint(x: Int32.random(in: 0..<maxPosition.width), y: Int32.random(in: 0..<maxPosition.height) - 1)
        self.maxPosition = maxPosition

        self.length = initialLengthFunc()
    }

    mutating func update() {
        length -= 1
        position.y += 1

        if length == 0 {
            self = MatrixClearString(maxPosition: maxPosition)
        }
    }
}

setlocale (LC_ALL, "")
initscr()


var screenSize = NCursesSize(width: getmaxx(stdscr), height: getmaxy(stdscr))

var matrixStrings: [MatrixString] = [
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize)
]

let waitForCharacterTimeout: Int32 = 50
start_color()
noecho()
timeout(waitForCharacterTimeout);
curs_set(0)
init_pair(1, Int16(COLOR_GREEN), Int16(COLOR_BLACK))
clear()

var lastChar: Int32 = -1
while(lastChar != "x".utf8CString[0]) {
    for i in matrixStrings.indices {
        matrixStrings[i].draw()
        matrixStrings[i].update()
    }

    refresh()
    lastChar = getch()
}

endwin()

How does it look?

Pretty dang cool. This has been an itch I’ve wanted to scratch for a while and now I’ve learned how to do this, plus in Swift, and on Linux.

Wake up Neo…

I was doing some cleanup on my computer and came across an old program I had made in 2001 while I was still a young ‘un. Let’s see what it does…

That’s actually pretty cool! Time to ship it to production, but first let’s give it the ol’ PR treatment.

Sort of a picky PR person! Let’s address some of these points.

  1. Email does not work
    • Blame Lord Black
  2. Alphabetize headers please
    • Agree…
  3. What do these do?
    • I didn’t know what I was doing…
  4. What do these do and what do the numbers mean?
    • Well the ‘5’ is obviously the length of string one…
  5. Ditto
    • Ditto!
  6. Blah blah blah…

Sort of a jerk, that PR person…

Some points were raised though. I could go back and fix them but that means going and installing Turbo-C++… or trying to modernize it…

I’m an iOS dev now… this console based stuff is outside of my wheelhouse but I don’t have a clue how I’d put char ‘Z’ at point (X, Y) in the terminal. It’d be fun to learn how to do so. There’s this library called ncurses I’ve heard about. So let’s modernize it!

So we create a swift macOS command line app, link our binary against libncurses.tbd and… what the heck do I import?

Some googling and I come across this super helpful video: “https://www.youtube.com/watch?v=syCz6CmzTN8”. Cool we’ve got a hello world up on our screen!

This looks like we could almost do a straight port of the code above… lets see how close we can get.

Pretty close! Here’s how it looks:

And here’s the code:

import Darwin.ncurses

var string_one_length: Int32 = 5
var string_two_length: Int32 = 7
var string_three_length: Int32 = 9
var string_four_length: Int32 = 12
var clear_string_one_length: Int32 = 5
var clear_string_two_length: Int32 = 5
var clear_string_three_length: Int32 = 5
var clear_string_four_length: Int32 = 5

var string_one_x: Int32 = 5
var string_one_y: Int32 = 7
var string_two_x: Int32 = 9
var string_two_y: Int32 = 12
var string_three_x: Int32 = 5
var string_three_y: Int32 = 7
var string_four_x: Int32 = 9
var string_four_y: Int32 = 4
var clear_string_one_x: Int32 = 5
var clear_string_one_y: Int32 = 5
var clear_string_two_x: Int32 = 5
var clear_string_two_y: Int32 = 5
var clear_string_three_x: Int32 = 5
var clear_string_three_y: Int32 = 5
var clear_string_four_x: Int32 = 5
var clear_string_four_y: Int32 = 5


initscr()
start_color()
noecho()
curs_set(0)
init_pair(1, Int16(COLOR_GREEN), Int16(COLOR_BLACK))
clear()

while(true) {
    attron(COLOR_PAIR(1))
    clear_string_one();
    string_one();
    clear_string_two();
    string_two();
    clear_string_three();
    string_three();
    clear_string_four();
    string_four();
    attroff(COLOR_PAIR(1))
    refresh()
    usleep(10000)
}

func string_one() {

    let string_one_char =  UInt32.random(in: 0..<229) + 27;
    move(string_one_y, string_one_x) // Watch out! These are switched!
    addch(string_one_char)

    string_one_length = string_one_length - 1;
    string_one_y = string_one_y + 1;

    if (string_one_length == 0) {
        string_one_length = Int32.random(in: 0..<10) + 4;
        string_one_x = Int32.random(in: 0..<80);
        string_one_y = Int32.random(in: 0..<39);
    }
}

func string_two() {

    let string_two_char =  UInt32.random(in: 0..<229) + 27;
    move(string_two_y, string_two_x)
    addch(string_two_char)

    string_two_length = string_two_length - 1;
    string_two_y = string_two_y + 1;

    if (string_two_length == 0) {
        string_two_length = Int32.random(in: 0..<10) + 4;
        string_two_x = Int32.random(in: 0..<80);
        string_two_y = Int32.random(in: 0..<39);
    }
}

func string_three() {

    let string_three_char =  UInt32.random(in: 0..<229) + 27;
    move(string_three_y, string_three_x)
    addch(string_three_char)

    string_three_length = string_three_length - 1;
    string_three_y = string_three_y + 1;

    if (string_three_length == 0) {
        string_three_length = Int32.random(in: 0..<10) + 4;
        string_three_x = Int32.random(in: 0..<80);
        string_three_y = Int32.random(in: 0..<39);
    }
}

func string_four() {

    let string_four_char =  UInt32.random(in: 0..<229) + 27;
    move(string_four_y, string_four_x)
    addch(string_four_char)

    string_four_length = string_four_length - 1;
    string_four_y = string_four_y + 1;

    if (string_four_length == 0) {
        string_four_length = Int32.random(in: 0..<10) + 4;
        string_four_x = Int32.random(in: 0..<80);
        string_four_y = Int32.random(in: 0..<39);
    }
}

func clear_string_one() {
    move(clear_string_one_y, clear_string_one_x)
    addch(UInt32(" "))

    clear_string_one_length = clear_string_one_length - 1;
    clear_string_one_y = clear_string_one_y + 1;

    if (clear_string_one_length == 0) {
        clear_string_one_length = Int32.random(in: 0..<10) + 4;
        clear_string_one_x = Int32.random(in: 0..<80);
        clear_string_one_y = Int32.random(in: 0..<39);
    }
}

func clear_string_two() {
    move(clear_string_two_y, clear_string_two_x)
    addch(UInt32(" "))

    clear_string_two_length = clear_string_two_length - 1;
    clear_string_two_y = clear_string_two_y + 1;

    if (clear_string_two_length == 0) {
        clear_string_two_length = Int32.random(in: 0..<10) + 4;
        clear_string_two_x = Int32.random(in: 0..<80);
        clear_string_two_y = Int32.random(in: 0..<39);
    }
}

func clear_string_three() {
    move(clear_string_three_y, clear_string_three_x)
    addch(UInt32(" "))

    clear_string_three_length = clear_string_three_length - 1;
    clear_string_three_y = clear_string_three_y + 1;

    if (clear_string_three_length == 0) {
        clear_string_three_length = Int32.random(in: 0..<10) + 4;
        clear_string_three_x = Int32.random(in: 0..<80);
        clear_string_three_y = Int32.random(in: 0..<39);
    }
}

func clear_string_four() {
    move(clear_string_four_y, clear_string_four_x)
    addch(UInt32(" "))

    clear_string_four_length = clear_string_four_length - 1;
    clear_string_four_y = clear_string_four_y + 1;

    if (clear_string_four_length == 0) {
        clear_string_four_length = Int32.random(in: 0..<10) + 4;
        clear_string_four_x = Int32.random(in: 0..<80);
        clear_string_four_y = Int32.random(in: 0..<39);
    }
}

Now let’s finally address those PR comments…

//
//  main.swift
//  matrixTwoDotOh
//
//  Created by Daniel Drzimotta on 2020-05-07.
//  Copyright © 2020 Daniel Drzimotta. All rights reserved.
//

import Darwin.ncurses

struct NCursesPoint {
    var x: Int32
    var y: Int32
}

struct NCursesSize {
    var width: Int32
    var height: Int32
}

protocol MatrixString {
    var position: NCursesPoint { get }
    var length: Int32 { get }
    var displayCharacter: UInt32 { get }

    mutating func update()
}

let initialLengthFunc = { Int32.random(in: 0..<10) + 4 }

// TODO: Would be nice to get this working! I have no clue how though
// With help from: https://scifi.stackexchange.com/a/182823
// I think I need ncursesw... Could I statically link it? Let's use just the
// characters on our keyboard for now...
//let characterSet = "日ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ012345789Z:・.\"=*+-<>¦|╌çリク"
let characterSet = "012345789ABCDEFGHIJKLMNOPQRSTUVWXYZ`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"

extension MatrixString {
    func draw() {
        mvaddch(position.y, position.x, displayCharacter)
    }

    static var initialLength: Int32 {
        Int32.random(in: 0..<10) + 4
    }
}

struct MatrixCharacterString: MatrixString {
    var position: NCursesPoint
    var maxPosition: NCursesSize
    
    var length: Int32
    var displayCharacter: UInt32 = UInt32(" ")

    init(maxPosition: NCursesSize) {
        self.position = NCursesPoint(x: Int32.random(in: 0..<maxPosition.width), y: Int32.random(in: 0..<maxPosition.height) - 1)
        self.maxPosition = maxPosition

        self.length = initialLengthFunc()
    }

    mutating func update() {
        displayCharacter = characterSet.randomElement()?.unicodeScalars.first?.value ?? 0
        length -= 1
        position.y += 1

        if length < 0 {
            self = MatrixCharacterString(maxPosition: maxPosition)
        }
    }
}

struct MatrixClearString: MatrixString {
    var position: NCursesPoint
    var maxPosition: NCursesSize

    var length: Int32
    var displayCharacter: UInt32 = UInt32(" ")

    init(maxPosition: NCursesSize) {
        self.position = NCursesPoint(x: Int32.random(in: 0..<maxPosition.width), y: Int32.random(in: 0..<maxPosition.height) - 1)
        self.maxPosition = maxPosition

        self.length = initialLengthFunc()
    }

    mutating func update() {
        length -= 1
        position.y += 1

        if length == 0 {
            self = MatrixClearString(maxPosition: maxPosition)
        }
    }
}

initscr()

var screenSize = NCursesSize(width: getmaxx(stdscr), height: getmaxy(stdscr))

var matrixStrings: [MatrixString] = [
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixCharacterString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize),
    MatrixClearString(maxPosition: screenSize)
]

let waitForCharacterTimeout: Int32 = 50

start_color()
noecho()
timeout(waitForCharacterTimeout);
curs_set(0)
init_pair(1, Int16(COLOR_GREEN), Int16(COLOR_BLACK))
clear()
attron(COLOR_PAIR(1))

while(getch() == ERR) {
    for i in matrixStrings.indices {
        matrixStrings[i].draw()
        matrixStrings[i].update()
    }

    refresh()
}

Cool! Turns out the chars that we were displaying in the DOS version are all over the place when using ncurses. (https://en.wikipedia.org/wiki/Code_page_437) I looked at displaying the original matrix characters but the version of ncurses included with macOS doesn’t seem to support them. I just decided to display the characters that I see on my keyboard. So how does it look?

Not bad! This was a fun little project.

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!

An ode to the responder chain ❤️

The responder chain doesn’t get a whole lot of love anymore. It sits on the sidelines as ol’ dependable, helping your app out under the hood while rarely being the first choice of communication for your UI events within your app.

In the early days of iPhone OS (as it was called back then) the responder chain was the easy way to communicate UI events outside of your view or view controller without having to explicitly name a target, delegate or notification and all the associated code & setup that came with those approaches. Sure sometimes events would get lost due to programmer error, but if after a refactoring and your button stopped working having been tapped you had an “oh… right… missed that” moment and quickly fixed it.

Then something happened… the iPad came out with all of these fancy, new gesture recognizers allowing you to easily know when a tap, rotation, pinch or zoom occured, with a whole new way of knowing when those events occurred. It kicked the responder chain right to the curb, only communicating though explicit target-action pairs.

These were the new hotness. Everybody started using it and gesture recognizers being a lot more easy to learn quickly became the default for new people getting acquainted with iOS development. Why put in all this effort to learn about next responder, the responder chain, making sure your view controllers are properly a child of their parent view controller and all that jazz if you can just set a target-action pair and ship?

This changed something though! The responsibility of setting up a view to emit an event shifted from being purely encapsulated within that view or view controller to having to be set up from outside of that view or view controller. While not bad it could be overkill if you just want to signal an event, and can lead to slightly coupled code.

Let’s do a somewhat contrived example. A common requirement within apps is to ask the customer for permission to send push notifications. This is a good example as using the responder chain is great when you need to know an event happened within your app, but don’t care about what sent the event. You decide to ask for permission in your onboarding flow with the user having tapped a custom checkmark that is just a view with a tap gesture recognizer attached.

Your code might look like this:

class OnboardingView: UIView {
    let enableNotificationsCheckmarkView: CheckmarkView
    ...
}

class OnboardingViewController: UIViewController {
    private let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let enableNotificationsRecognizer = UITapGestureRecognizer(target: self,
                                                                   action: #selector(handleEnableNotifications(_:)))

        onboardingView.enableNotificationsCheckmarkView.addGestureRecognizer(enableNotificationsRecognizer)
    }

    @objc func handleEnableNotifications(_ recognizer: UIGestureRecognizer) {
        // Ask user for permission for notifications
        ...
    }
}

Pretty simple… but then a business requirement comes up to also put this in the more menu. Now you’ve got some choices to make… Do you create a NotificationsManager type object and call that from within handleEnableNotifications(_:)? Now your OnboardingViewController needs to know about NotificationsManager as does your more menu code. If you’re into dependency injection that comes with some extra complexity.

Let’s consider a delegate. You’ll have to define a protocol, create a weak delegate in your OnboardingViewController, call that delegate from handleEnableNotifications(_:) and do something similar in your more menu code. Depending on where you are setting the delegate from you may have to break some encapsulation as well. If you’ve found yourself making a delegate for a delegate to get around this… you’ve delved too deep and monsters await. 👻 Similar issues will occur if you decide to break encapsulation and add a tap gesture recognizer from outside of the OnboardingViewController, and ditto if you pass in a callback.

Let’s consider a notification. This isn’t bad but you’ll still need to have this tap gesture recognizer fire the notification, have the notification name defined, and have registered to receive those notifications. This isn’t brittle, but there’s a way with even less code.

Wouldn’t it be nice if our onboarding view could just shout to the world (or app), “Hey! Gimme some notification permissions! I don’t care how it’s done!”. If our checkmark was a UIButton we could actually do this:

class OnboardingView: UIView {
    let enableNotificationsCheckmarkButton: CheckmarkButton
    ...
}

class OnboardingViewController: UIViewController {
    let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        onboardingView.enableNotificationsCheckmarkButton.addTarget(nil,
                                                                    action: Selector(("handleEnableNotifications")),
                                                                    for: .primaryActionTriggered)
    }
}

This will walk the responder chain from the button, to the OnboardingView, to the OnboardingViewController, to its parent view controller, to the UIApplication object, and then finally to the UIApplicationDelegate, checking each object if it responds to the method handleEnableNotifications. This link has a very good description.

Just to be complete our AppDelegate may look like this:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    @objc func handleEnableNotifications() {
        // Handle our notification permission query
        ...
    }
}

People into compile time safety are probably having 🚨 big red warning signals 🚨 flashing in their mind right now with the use of Selector(("handleEnableNotifications")). Swift is all about compile time safety, and with a bit of extra code we can get that, otherwise we run the risk of fat-fingering the method to call when that button is tapped, and also the implementation. This was a risk back in early iOS, but thankfully we can rely on the swift compiler to warn us against those incidents.

Here’s how all the code would look with proper compile time safety:

    @objc protocol EnableNotificationsResponder: AnyObject {
    func handleEnableNotifications()
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, EnableNotificationsResponder {

    func handleEnableNotifications() {
        // Handle our notification permission query
        ...
    }
}

class OnboardingView: UIView {
    let enableNotificationsCheckmarkButton: CheckmarkButton
    ...
}

class OnboardingViewController: UIViewController {
    let onboardingView = OnboardingView()

    override func loadView() {
        self.view = onboardingView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        onboardingView.enableNotificationsCheckmarkButton.addTarget(nil,
                                                                    action: #selector(EnableNotificationsResponder.handleEnableNotifications),
                                                                    for: .primaryActionTriggered)
    }
}

Our more menu can do something similar, and as long as it sends the same call up the responder chain it will be handled in the same way. When version 2.0 of your app comes out with a completely redesigned interface you won’t be messing with delegate hookups, callbacks having to be injected, and new classes having to be aware of a potential NotificationsManager. Just fire up the responder chain and forget.

Sometimes we are not able to use a UIButton or related control without a target that uses the responder chain. You can fire your own events using ‘UIApplication.shared.sendAction’. Let’s see how you would do that if you only wanted to fire up the responder chain when a touch ended on a view:

    class SecondOnboardingView: UIView {
    ...

    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)

        UIApplication.shared.sendAction(#selector(EnableNotificationsResponder.handleEnableNotifications(_:)), to: nil, from: self, for: nil)
    }
}

class SecondOnboardingViewController: UIViewController {

    override func loadView() {
        let view = UIView()

        view.backgroundColor = .white

        let secondOnboardingView = SecondOnboardingView()
        secondOnboardingView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(secondOnboardingView)

        NSLayoutConstraint.activate([
            secondOnboardingView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            secondOnboardingView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
            secondOnboardingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            secondOnboardingView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])

        self.view = view
    }
}

These are easy to test too as it is so easy to wrap the button, view or view controller that sends these event in something that is itself part of the responder chain. You just need to have something wrap the UIResponder that fires the event, and then capture it. Best of all… no mocking! Check it out:

    class OnboardingViewControllerTest: XCTestCase {

    var subject: ResponderCatcher!
    var onboardingViewController: OnboardingViewController!

    override func setUp() {
        subject = ResponderCatcher()

        onboardingViewController = OnboardingViewController()

        subject.addChild(onboardingViewController)
        subject.view.addSubview(onboardingViewController.view)
        onboardingViewController.didMove(toParent: subject)
    }

    func testOnboardingCheckmarkButton() {
        onboardingViewController.onboardingView.enableNotificationsCheckmarkButton.sendActions(for: .primaryActionTriggered)

        assert(subject.caughtResponderAction, "Expected to have caught our responder action")
    }
}


class ResponderCatcher: UIViewController, EnableNotificationsResponder {
    var caughtResponderAction = false

    func handleEnableNotifications(_ sender: Any?) {
        caughtResponderAction = true
    }
}

There is so much you can do with the responder chain. You can define responder actions in interface builder, modify the responder chain away from it’s default flow, make it do your laundry… (almost…) It definitely should be under consideration when you build an app.