2015-03-02

Object-oriented State pattern with view controllers in Objective-C and iOS

I have a UIViewController subclass that is controlling a view hierarchy which represents a portion of the UI. That portion of the UI can be in one of three modes: browse, edit, or empty. These modes can change back and forth at run time by user actions.

Right now I have code in every method of the view controller that looks like this:

NSAssert(self.mode == SBModeBrowse || self.mode == SBModeEmpty, @"Should not be called in SBMode %d", self.mode);

And this:

if (self.mode != SBModeEmpty)
{
    [self performScrollCompletion];
}

And so on.

Even worse, I'm probably missing a few asserts and if-thens, and the code just happens to work at the moment.

It seems the object-oriented pattern that solves this situation is the State pattern.

It took me a while to figure out how to do this with UIViewController subclasses because the state behavior will affect UIControls wired up via IBOutlets to the view controller itself. How are the behavior classes supposed to manipulate these objects?

Also, I did not read the State pattern explanation closely enough, so I started off trying to subclass my view controller subclasses instead of merely adding a property to them for a behavior object.

I made a simple sandbox project to teach myself how to add the State pattern to view controllers. Here is my specification.
  • Display a UILabel with a number.
  • Display a UIButton.
  • Display a UIStepper.
  • Start in Browse mode.
  • In Browse mode:
    • Only button allows user taps.
    • Only label and button showing.
    • Button title is "Edit" and tapping it goes to Edit mode.
    • Stepper is invisible and does not allow taps.
  • In Edit Mode:
    • Stepper is visible and allows taps.
    • Stepper adjusts number in label.
    • Button title is "Browse" and tapping it does to Browse mode.
  • When exiting Edit mode, if the number in the label is less than 1, then enter Empty mode.
  • In Empty Mode:
    • Stepper is invisible and no taps allowed.
    • Label background is red.
    • Button tap moves to Edit mode.
Here are the classes and code to accomplish this by using the State pattern and view controllers.

InfoKeys.h


#ifndef SandBox3_InfoKeys_h
#define SandBox3_InfoKeys_h
static NSString *const keyButton = @"button";
static NSString *const keyStepper = @"stepper";
static NSString *const keyLabel = @"label";
#endif

ViewControlller.h


@class Behavior;
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *button;
@property (weak, nonatomic) IBOutlet UILabel *label;
@property (weak, nonatomic) IBOutlet UIStepper *stepper;
@property (strong, nonatomic) Behavior *behavior;
@property (assign, nonatomic) uint16_t labelValue;
- (IBAction)buttonTapped:(id)sender;
- (IBAction)stepperTapped:(id)sender;
@end

ViewController.m


#import "ViewController.h"
#import "Behavior.h"
#import "InfoKeys.h"

@implementation ViewController
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.behavior = [[Behavior alloc] initWithInfo:@{keyButton: self.button, keyLabel: self.label, keyStepper: self.stepper}];
}

- (IBAction)buttonTapped:(id)sender
{
    [self.behavior buttonTapped:@{keyButton: sender, keyStepper: self.stepper, keyLabel: self.label}];
}

- (IBAction)stepperTapped:(id)sender
{
    [self.behavior stepperTapped:@{keyStepper: sender, keyLabel: self.label}];
}
@end

Behavior.h


@interface Behavior : NSObject
- (instancetype)initWithInfo:(NSDictionary *)info;
- (void)buttonTapped:(NSDictionary *)info;
- (void)stepperTapped:(NSDictionary *)info;
@end

Behavior.m


#import "Behavior.h"
#import "BehaviorBrowse.h"
#import "BehaviorEdit.h"
#import "BehaviorEmpty.h"
#import "InfoKeys.h"

@implementation Behavior {
    Behavior *_mode;
}

- (instancetype)initWithInfo:(NSDictionary *)info
{
    self = [super init];
    if (self) {
        // Prevent problems if subclasses call this method (for example, as super in overrides).
        if ([self isMemberOfClass:[Behavior class]]) {
            _mode = [[BehaviorBrowse alloc] initWithInfo:info];
        }
    }
    return self;
}

- (void)buttonTapped:(NSDictionary *)info
{
    [_mode buttonTapped:info];
    UIStepper *stepper = info[keyStepper];
    // Coming back from Empty should allow mode to go to Edit so user can increase 0 value.
    if (stepper.value < 1 && ![_mode isKindOfClass:[BehaviorEmpty class]]) {
        _mode = [[BehaviorEmpty alloc] initWithInfo:info];
    }
    else {
        if ([_mode isKindOfClass:[BehaviorEdit class]]) {
            _mode = [[BehaviorBrowse alloc] init];
        }
        else { // Coming from Empty ends up here also.
            _mode = [[BehaviorEdit alloc] init];
        }
    }
}

- (void)stepperTapped:(NSDictionary *)info
{
    [_mode stepperTapped:info];
}
@end

BehaviorBrowse.m


#import "BehaviorBrowse.h"
#import "InfoKeys.h"

@implementation BehaviorBrowse

- (instancetype)initWithInfo:(NSDictionary *)info
{
    self = [super init];
    if (self) {
        UIButton *button = info[keyButton];
        UILabel *label = info[keyLabel];
        UIStepper *stepper = info[keyStepper];
        label.text = [NSString stringWithFormat:@"%.0f", stepper.value];
        stepper.alpha = 0.0;
        stepper.userInteractionEnabled = NO;
        // Button title shows user way to go to opposite mode.
        [button setTitle:@"Edit" forState:UIControlStateNormal];
    }
    return self;
}

- (void)buttonTapped:(NSDictionary *)info
{
    UIButton *button = (UIButton *)info[keyButton];
    // Button titles inform user of way back to the mode they are about to leave.
    [button setTitle:@"Browse" forState:UIControlStateNormal];
    UIStepper *stepper = (UIStepper *)info[keyStepper];
    [UIView animateWithDuration:0.3
                     animations:^(void){stepper.alpha = 1.0;}
                     completion:^(BOOL finished) {stepper.userInteractionEnabled = YES;}];
}

- (void)stepperTapped:(NSDictionary *)info
{
    NSAssert(NO, @"Stepper should not be tapable during browse mode.");
}

@end

BehaviorEdit.m


#import "BehaviorEdit.h"
#import "InfoKeys.h"

@implementation BehaviorEdit

- (void)buttonTapped:(NSDictionary *)info
{
    UIStepper *stepper = info[keyStepper];
    UIButton *button = info[keyButton];
    stepper.userInteractionEnabled = NO;
    // Let user know that button can bring them back to Edit from whatever mode comes next.
    [button setTitle:@"Edit" forState:UIControlStateNormal];
    [UIView animateWithDuration:0.3 animations:^(void){stepper.alpha = 0.0;}];
}

- (void)stepperTapped:(NSDictionary *)info
{
    UILabel *label = info[keyLabel];
    UIStepper *stepper = info[keyStepper];
    label.text = [NSString stringWithFormat:@"%.0f", stepper.value];
}

@end

BehaviorEmpty.m


#import "BehaviorEmpty.h"
#import "InfoKeys.h"

@implementation BehaviorEmpty

- (instancetype)initWithInfo:(NSDictionary *)info
{
    self = [super init];
    if (self) {
        UILabel *label = info[keyLabel];
        label.backgroundColor = [UIColor redColor];
        UIStepper *stepper = info[keyStepper];
        stepper.alpha = 0.0;
        stepper.userInteractionEnabled = NO;
    }
    return self;
}

- (void)buttonTapped:(NSDictionary *)info
{
    UIStepper *stepper = info[keyStepper];
    UIButton *button = info[keyButton];
    [button setTitle:@"Browse" forState:UIControlStateNormal];
    UILabel *label = info[keyLabel];
    label.backgroundColor = [UIColor lightGrayColor];
    [UIView animateWithDuration:0.3
                     animations:^(void){stepper.alpha = 1.0;}
                     completion:^(BOOL finished) {stepper.userInteractionEnabled = YES;}];
}

- (void)stepperTapped:(NSDictionary *)info
{
    NSAssert(NO, @"Stepper should not be tapable during empty mode.");
}


I am always interested in hearing better ways to do this.

2015-02-08

The Schwartzian Transform (from Perl) in Swift.

I realize that the "Schwartzian Transform" is not the name of algorithm itself but rather the name of the compact idiom in Perl for the expression of the algorithm. Since Swift has all of split(), join(), and map(), I will attempt to not only duplicate the algorithm, but the idiom itself.

As I write this, I am not certain I will succeed. I'm kind of excited. It is like a stage magic show.

I'm going off the 2006 article from Randal's web site.

Let's do the steps all broken down.

let data = "Bob 10\nSue 7\nJane 12\nBill 10\nJim 1\nNancy 8\n"
let lines = split(data, {$0 == "\n"})
let annotatedLines: Array = map(lines, {[$0, split($0, {$0 == " "})[1]]})
let sortedLines = sorted(annotatedLines, {$0[1] < $1[1]})
let cleanLines = map(sortedLines, {$0[0]})
let result: String = join("\n", cleanLines)
println(result)

Here is what it looks like with println() statements in a playground.


And here it is condensed properly to match the compact Perl idiom.

let data = "Bob 10\nSue 7\nJane 12\nBill 10\nJim 1\nNancy 8\n"
let result2 = join("\n",
    map(
        sorted(
            map(
                split(data, {$0 == "\n"}),
                {[$0, split($0, {$0 == " "})[1]]}),
            {$0[1] < $1[1]}),
        {$0[0]}))
println(result2)

And in the playground to prove it works.


Just so you know, as Xcode ran this code in the playground, my MacBook Pro (15-inch, Late 2011) CPU fans kicked in. I'm pretty sure Perl would not cause that to happen while processing the same data set.

Now, you may be thinking to yourself, "But it didn't work! Ten is not less than seven!" Yes, that is an obvious machine collating sequence problem because my version is doing string comparisons and not numeric.

This is where I think I've found a Swift defect. I can do this:


let stuff = "xxx 13"
let num:Int! = split(stuff, {$0==" "})[1].toInt()
println("num = \(num)")

But when I try to tack toInt() inside map() for annotatedLines, I get an error.


So the summary is that the Perl code is easier to read, it probably doesn't cause my CPU fans to engage, and its flexible scalar type is more convenient than the old integer vs string separation.

Up Next: the Orcish Maneuver.

Xcode build script: Change value in non-compiled file between Debug and Release.

I have a file that contains lines of text. One of those lines should to have a different value between Debug and Release. The file is not handled by the C preprocessor.

I am accomplishing this with an Xcode build script. If you know of something better, please tell me.

Here is the file and its contents.


I want the string "Production" for thing1 to be replaced with "Debug" during development. In my particular situation, if the script fails, it is better that the release value is used during development instead of the debug value escaping into the app store. Therefore, I use the "Production" string as the default value.

Look at Project | Target | Build Phases | Copy Bundle Sources.


I create a new build phase after this one. Select + button (tooltip: Add New Build Phase tool) | New Run Script Phase.


Put the Run Script after Copy Bundle Resources.


The next step unfortunately involves intimate knowledge of shell syntax. To create your own scripts to do different things, you will also be digging around in the Build Setting Reference doc.

Here is the script that does my task relatively safely.


I doubt anyone wants to cut and paste that, but just in case:

echo "CONFIGURATION = $CONFIGURATION"
if [ "$CONFIGURATION" == "Debug" ]; then
    f1="Statham.notJson"
    echo "CONFIGURATION_BUILD_DIR = $CONFIGURATION_BUILD_DIR"
    echo "CONTENTS_FOLDER_PATH = $CONTENTS_FOLDER_PATH"
    f2="$CONFIGURATION_BUILD_DIR/$CONTENTS_FOLDER_PATH/$f1"
    if [ -f $f2 ]; then
        sed -i "" -e '/thing1:/s/Production/Debug/' "$f2"
    else
        echo "File not found: $f2"
    fi
else
    echo "CONFIGURATION is not Debug. Not updating $f1."
fi

I like the echoes in that script because they help to debug it by looking at the build logs.


Here is my Terminal output to test that my changes worked.

$ pwd
/Users/jeff/Library/Developer/Xcode/DerivedData/SandBox2-aiqtaeeaiietgzagreaqpyganmtt/Build/Products/Debug-iphonesimulator/Pen1.app
$ ls -l
total 72
drwxr-xr-x  6 jeff  staff    204 Feb  8 10:11 Base.lproj
-rw-r--r--  1 jeff  staff    920 Feb  8 10:11 Info.plist
-rwxr-xr-x  1 jeff  staff  21240 Feb  8 10:11 Pen1
-rw-r--r--  1 jeff  staff      8 Feb  8 10:11 PkgInfo
-rw-r--r--  1 jeff  staff     67 Feb  8 10:11 Statham.notJson
$ cat Statham.notJson 
# Whatever file.
    thing0: 76
    thing1: "Debug"
oddItem: tulip

Of course, my build settings were Debug at the time. Here is my scheme to prove it.


Let's make sure the changes do not happen if we are building for Release. I update my scheme and build again. The first thing I notice is a difference in the build logs.


That looks good, but let's check for real, but be sure I'm in the right directory when I look.

$ pwd
/Users/jeff/Library/Developer/Xcode/DerivedData/SandBox2-aiqtaeeaiietgzagreaqpyganmtt/Build/Products
$ ls -l
total 0
drwxr-xr-x  4 jeff  staff  136 Feb  7 16:16 Debug-iphoneos
drwxr-xr-x  7 jeff  staff  238 Feb  8 10:11 Debug-iphonesimulator
drwxr-xr-x  6 jeff  staff  204 Feb  8 10:26 Release-iphonesimulator
$ cat Release-iphonesimulator/Pen1.app/Statham.notJson 
# Whatever file.
    thing0: 76
    thing1: "Production"
oddItem: tulip

It worked!

Now, who can spot the (fairly harmless) defect in my shell script?

2015-02-07

How to embed private Objective-C framework into iOS app on Xcode 6.

I'm creating this post since Apple's documentation on this topic is an out-of-date lie that mentions Xcode 2.4. And Stack Overflow is overrun with point collectors who can only answer minor variations of the same question over and over again.

The simplest form of this recipe is easy once you get past a few snags. I hope this helps someone some day before it also falls out of date.

Warning: this only works for iOS 8 or better. I'm not sure the linker should seg fault because of this though.


I start with a project named Pen1 in a workspace named SandBox2.


Elsewhere, I have already created a Cocoa Touch Framework like this.


The framework is a separate project from the SandBox2 workspace. I did this to simulate the scenario of:

  • You created some code in one project.
  • You have to add the code to another project.
  • You thought to yourself, "I'll turn this small chunk of code into a framework."
  • So then you create a separate project to house the framework with the idea of adding it into other projects and workspaces later.


The framework is named BobLib. It consists of 3 main files: BobLib.h, Newhart.h, and Newhart.m. Here are the contents of those files.



I want to use the method doThatThing from the Newhart class in my Pen1 project.

Next is something you must do, and I've only ever seen it mentioned in a WWDC 2014 video. You must select the Newhart.h file in the Project Navigator. Then in the Utilities Area, select the File Inspector. Inside the Target Membership section, you will have to change the value from "Project" to "Public".


If you don't do this, then after you add BobLib to Pen1, the compile of Pen1 will fail because it can't find Newhart.h.



I build BobLib for both simulator and device. After the build, wait for Xcode to index files, and then BobLib.framework in the Project Navigator under Products group will turn from red text (not found, I suppose) to black text. Control-click BobLib.framework and select Show in Finder from the menu.



I open SandBox2 in Xcode. I drag the simulator build of BobLib.framework in Finder into Pen1 in Xcode.


I guess the options to select here are based on what you are doing with your source code control system. I have not worked out the best thing to do for my own environment yet. For this example, I accepted the default choices.


In the project target General settings, I can see that Xcode automatically added the framework to the Linked Frameworks and Libraries section. Thank you, Xcode.


And it will build successfully, but when I ran it in the simulator, I hit this:


With this error message:

dyld: Library not loaded: @rpath/BobLib.framework/BobLib Referenced from: /Users/jeff/Library/Developer/CoreSimulator/Devices/EF59ACE7-ACD5-40CB-92ED-A4BBBBE619F0/data/Containers/Bundle/Application/7151B3F9-A962-44C0-A5A8-84B4AB619697/Pen1.app/Pen1 Reason: image not found

What Xcode did not do is add BobLib to the Embedded Binaries section of the project target General settings. I am not certain if that makes sense or not since I am so new to embedded frameworks.


Hit the + button, add BobLib.framework, and build and run again.

For some reason, after adding it to Embedded Binaries, Xcode adds it again to Linked Frameworks and Libraries. I delete that extra one.


Here is my code in Pen1 that will test if the app can use BobLib. Since I only copied the framework build for simulator, I can only test this out on Simulator.


After doing the above steps. I know the app has successfully used the framework by checking the console.


One final note. The WWDC 2014 video Building Modern Frameworks is bloody frustrating. The speaker is too enamored of himself, but this would be tolerable if he provided useful information. It is mostly slideshow in which he repeats the stuff we all know from the method naming guidelines. He drones on for 45 minutes before we get to see him play around in Xcode for 10 minutes, and then he casually races past crucial details that are not documented anywhere else on Earth but in this damned video.

2014-11-23

Xcode 6.1.1 GM Seed and _kLSApplicationLockedInStoppedStateKey.

This just cost me an hour or so, and I see little mention of it on the Internet.

Because of bugs involving Swift in Xcode 6.1 (Build: 6A1052d) I was desperate to move to Xcode 6.1.1 pre-release version.

I uninstalled Xcode 6.1. I scanned for other Xcode-related files left behind and deleted ones that had a high likelihood of not being essential. For example, I left /usr/bin/xcodebuild alone because it is part of the com.apple.pkg.Essentials package whereas I deleted the files in /Users/me/Library/Developer/Xcode.

I downloaded and installed Xcode 6.1.1. After installation, I tried to start it. A small window appeared that floated on top of everything. It told me that Xcode was being verified. The progress bar took a long time to finish and several moments after it had finished, a dialog popped up to replace the tiny verify progress window. The dialog was the usual warning that you have downloaded this app from the Internet, are you sure you want to open it? I clicked the button that indicated yes, I want to open the app.

Nothing useful happens. After restarting, trying it all again, getting the same result, I found this log entry on the Console:

11/23/14 6:32:47.962 PM launchservicesd[54]: Someone attempted to start application App:"Xcode" asn:0x0-2c02c pid:367 refs=5 @ 0x7f81cae47bc0 but it still has _kLSApplicationLockedInStoppedStateKey=true, so it is is staying stopped. : LASApplication.cp #2517 SetApplicationInStoppedState() q=LSSession 100005/0x186a5 queue

I fiddled around trying different things, and with every attempt I had to wait a long time for the Xcode verification process to finish before finding out if my attempt was successful or not. I even went into System Preferences > Security & Privacy and set "Allow apps downloaded from:" to the value "Anywhere". That did not help. I really hope I remember to set it back.

If you search the Apple Developer Forums for "_kLSApplicationLockedInStoppedStateKey" you'll get one hit from August this year. Reading the post and response, it seems like it was just a one time thing for that user. He kept trying to open Xcode, and eventually it worked!

I gave it a shot. I clicked the Xcode icon in the Dock maybe 20 or 30 times in rapid succession. And it seemed to have worked! Xcode opened! I can open my project in Xcode again. I'm scared to press Build. I'm scared to close the Xcode app. Right now, I'm very scared to run the Simulator.

Right now, I am traumatized. I rarely have this kind of opaque problem with Apple products, even their development tools. This is the first time I've use pre-release development tools, however. It was a horrible reminder of being back on other OSs where I spent too much of my time wrestling with it instead of being productive. I really hate that.

2014-11-20

My Swift equivalent of C macro DLog() with __PRETTY_FUNCTION__.

I did not see this exact thing out in the wild yet. It is cobbled together from unpopular Stack Overflow answers and highly informative blog posts.

func dLog(message: String, fullPath: String = __FILE__, line: Int = __LINE__, functionName: String = __FUNCTION__) {
    let filename = fullPath.lastPathComponent
    // Remove ".swift" from file name.
    let splitFilename = split(filename, {(c:Character)->Bool in return c=="."}, allowEmptySlices: false)
    let classGuess = splitFilename[0]
    // Remove "()" from function name.
    let splitFuncName = split(functionName, {(c:Character)->Bool in return c=="("}, allowEmptySlices: false)
    let funcName = splitFuncName[0]
    NSLog("%@", "[\(classGuess) \(funcName)] [Line \(line)] \(message)")
}

The output looks like this.

2014-11-19 18:55:05.887 SandboxApp[12433:287724] [SomeViewController viewDidLoad] [Line 27] dLog message test.

Note that the biggest flaw is the use of the file name instead of the class name. According to Swift docs there is no __CLASS__ special literal expression.

To make this global, I dropped it in the AppDelegate file outside of the AppDelegate class. There has got to be a better convention for global functions, but I am only burning one bridge at a time.

For bonus points, the Xcode autocomplete on the split() function crashed SourceKitService. I guess this means I'm cutting edge now.


2014-10-11

GIMP script (Scheme) to scale multiple files at once (batch).

I made this back in January of this year, but I forgot to post it. I searched around briefly, but at the time, I found nothing that I could just drop in and use. So I had to write this.

It is written in Scheme. On my MacBook, I drop this script in ~/Library/Application Support/GIMP/2.8/scripts. Then from the GIMP menu bar select Filters | Script-Fu | Refresh Scripts. This script should then show up under the GIMP menu bar selection Image | Transform | Batch Scale By Factor.

There are a lot of reasons we would need to perform this a batch scale of images files, but mine was related to iOS development. Retina images must be twice the resolution of non-Retina images in order to look good. So I used Retina images as originals and then I had to scale everything to half that size for non-Retina.

The script does not overwrite the original files. It makes new files with "_.jpg" appended to the file name, even if the file name already ends in ".jpg". Which reminds me, this script saves all files as JPEGs, even if the originals are not. If you want different output, I'm sure you can figure out how to change it.

Hey, even though I wrote this to not overwrite the originals, I highly recommend that you never operate on originals. Make a copy of your originals in a temp directory and run the batch process on the copies.

(define (script-fu-scale-by-factor fileglob factor)
  (let*
    (
      (files (cadr (file-glob fileglob 0)))
    )
    (do-scale-to files factor)
  )
)

(define (do-scale-to files factor)
  (while (not (null? files))
    (let*
      (
        (file (car files))
        (image (car (gimp-file-load 1 file file)))
        (width (car(gimp-image-width image)))
        (height (car(gimp-image-height image)))
        (newfile (string-append file "_.jpg"))
        (drawable (car (gimp-image-get-active-layer image)))
      )
      (gimp-image-scale image (* width factor) (* height factor))
      (gimp-file-save 1 image drawable newfile newfile)
    )
    (set! files (cdr files))
  )
)

(script-fu-register
  "script-fu-scale-by-factor"               ;func name
  "Batch Scale By Factor"                   ;menu label
  "Scale multiple images by given factor."  ;description
  "Jeffery Martin"                          ;author
  "Copyright 2014\
   Fluffy White Puppy Software"             ;copyright notice
  "2014-01-06"                              ;date created
  ""                                        ;image type that the script works on
  SF-STRING "fileglob" "/tmp/photos/image-*.jpg"
  SF-VALUE "factor" "0.5"
)

(script-fu-menu-register "script-fu-scale-by-factor" "<Image>/Image/Transform")

Here is what it looks like in operation. Original images files:



Here is the dialog when you run the script.



And the directory after the script run.



And notice that the image files are half the size because I input a scaling factor of 0.5 in the script dialog.