20 Cocoa Apps Challenge

Cocoa

App 04: Pick a Color v2

  • Array, Subscript syntax, Range
  • NSPopUpButton: Inserting menu items
  • Initialization Function

Internal Data

In this version we switch from static menu items (because they are created inside Xcode's Interface Builder one by one) to an internal array (they live together with our code). The array's value will become:

  1. the popup button menu item's title, and
  2. the basis for which images gets to be displayed

The first thing to do is to delete the menu items in Xcode's Interface Builder.

Then we create the property that contains an array of String. These strings in the array corresponds to an image file's name in Assets.xcassets.

Index and Element Count

A quick detour here.

A fundamental thing you need to know about arrays is that an array's highest index is always one less than the number of its elements.

For example, our array here contains 6 elements, so its highest index is 5.

The reason is because arrays start at index 0, therefore, its highest index will always be 1 less than the number of its elements. Always keep this in mind when accessing arrays; it will save you countless headaches.

End of quick detour.

NSPopUpButton

The next thing to do is to repopulate the popup button with the same values (titles) as in the previous version of our app:

Subscript Syntax

Accessing elements in an array is done through subscripts inside a pair of square brackets. The first element is always index 0.

An index can also come in the form of a range.

Closed Range

1...5
From 1 to 5, how much did you like the movie?

Closed range means a rating of either a 1 star, 2 stars, 3 stars, 4 stars, or 5 stars.

Closed range is inclusive. It includes the upper bound.

Half-Open Range

1..<21
Persons under 21 are not allowed inside the club.

Half-open range means any person who is between 1 to 20 years old is not allowed inside. If you are 21 years old and above you can come in.

Closed range is exclusive. It excludes the upper bound.

Range Index

Looking at the figure, you probably know where this leads to.

But alas, it does not work if you use it as an argument inside addItemsWithTitles() like so:

color.addItemsWithTitles( colors[1...5] )

This is because of Swift's strongly typed nature. You see, colors[1...5] returns an ArraySlice<String> type, not Array<String>!

Swift does not allow our colors property to be assigned any other type than Array<String> (or the syntactic-sugar equivalent [String]). ArraySlice<String> is not the same type as Array<String> (from here on [String]).

Type conversion again to the rescue:

We're almost done. Although this technique won't affect how the app works but it's always important to know and follow programming best practices.

Our colors[1...5] can still be improved by exchanging the upper bound 5 (which is static) with a dynamic one. This can prove its worth when your code's line count grows very long. Let me tell you why.

Suppose business is booming such that customers are clamoring for more color varieties. You decide to add 8 new colors:

let colors = ["Neutral","White","Black","Orange","Red","Yellow",
     "Blue","Amber","Ruby","Green","Pink","Lime","Bronze","Magenta"]

But schedule is so tight that you're forced to update the app immediately. The problem is, you forgot to update the number of your range index! It still looks like this: colors[1...5] when it should have been colors[1...13].

Surely there has to be a better way! And indeed there is. Remember the quick detour earlier?

An array's highest index is always one less than the number of its elements.

If our goal is to set the upperbound to 13, we could do it two ways...

The first one would be to use the closed range:

let upperBound = colors.count - 1
colors[1...upperBound]

The second and preferred way would be to use the half-open range:

colors[1..<colors.count]
    

With this, no matter how often and how many colors you add or remove from the colors array, you do not need to find and update the colors[1...13] everytime anymore. All that's needed was to change the static upper bound number to a dynamic one.

Control Initialization Function

Now that we have the code for populating the popup button control, the next question is where to put them such that when the app is launched it will actually populate the popup button control.

We cannot put the codes inside onColorSelect() because it only gets called when the popup button menu is selected. We need a way to be able to run codes inside a function that gets called when the app is launched but before it opens. In other words, a function for initializing UI controls.

It turns out that there is such a function: awakeFromNib().

Classes can implement this method to initialize state information after objects have been loaded from an Interface Builder archive (nib file).
override func awakeFromNib() {

  color.addItemWithTitle("Pick a color")

  let upperBound = colors.count
  color.addItemsWithTitles( Array( colors[1..<upperBound] ) )

}

With this, our popup button gets populated even before the app is launched. Problem solved.

How To

Code

//
//  AppDelegate.swift
//  Pick a color
//  Created by Arthur Kho (github.com/islandjoe) on 08/16.
//
//  MIT License
//  Copyright (c) 2016 Arthur Kho
//

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

  let colors = ["Neutral","White","Black","Orange","Red","Yellow"]

  @IBOutlet weak var window: NSWindow!
  @IBOutlet weak var color: NSPopUpButton!
  @IBOutlet weak var shirt: NSImageView!

  @IBAction func onColorSelect(sender: NSPopUpButton) {

    let selectedColor = color.indexOfSelectedItem
    let shirtColor    = colors[selectedColor]

    shirt.image    = NSImage(named: shirtColor)

  }

  override func awakeFromNib() {

    color.addItemWithTitle("Pick a color")
    color.addItemsWithTitles( Array( colors[1..<colors.count] ) )

  }

}

20 Cocoa Apps Challenge