You are on page 1of 474

iOS Development A Practical Approach 2nd Edition

iOS Development

iOS Development A Practical Approach 2nd Edition


Nicolaas tenBroek AMDG

2011, 2012

Edition Prepared 23 February 2012

Nicolaas tenBroek (AMDG)

iOS Development

Contents

Contents
iOS Development A Practical Approach 2nd Edition ...................................................................................... 1 Contents ........................................................................................................................................................ 2 Introduction .................................................................................................................................................. 5 Language Basics ............................................................................................................................................ 6 Data Types................................................................................................................................................. 7 Operators .................................................................................................................................................. 9 Control Structures ................................................................................................................................... 13 Iteration .................................................................................................................................................. 15 Preprocessor ........................................................................................................................................... 17 Classes and Objects ..................................................................................................................................... 24 Classes ..................................................................................................................................................... 25 The Self Reference .................................................................................................................................. 27 Access Specifiers ..................................................................................................................................... 28 Properties................................................................................................................................................ 29 Methods .................................................................................................................................................. 35 Blocks ...................................................................................................................................................... 40 Objects .................................................................................................................................................... 42 Basic User Interface in iOS .......................................................................................................................... 57 Model View Controller ............................................................................................................................ 57 View and Controller Life Processes ......................................................................................................... 59 Interface Builder ..................................................................................................................................... 61 IBOutlet ................................................................................................................................................... 71 IBAction ................................................................................................................................................... 75 Delegates ................................................................................................................................................ 80 UIView ..................................................................................................................................................... 84 UILabel .................................................................................................................................................... 86 UITextField .............................................................................................................................................. 87 UIButton .................................................................................................................................................. 89 UI Control Overview................................................................................................................................ 90 Protocols, Delegates, and Categories ......................................................................................................... 94 Delegates ................................................................................................................................................ 94

Nicolaas tenBroek (AMDG)

iOS Development

Contents

Protocols ................................................................................................................................................. 94 Categories ............................................................................................................................................. 102 Tables ........................................................................................................................................................ 113 Creating a Basic Table ........................................................................................................................... 117 Creating a Table with Sections .............................................................................................................. 124 Displaying a Table Index........................................................................................................................ 133 UITableViewDelegate............................................................................................................................ 139 Editing ................................................................................................................................................... 141 Custom Cells.......................................................................................................................................... 152 UITableViewController.......................................................................................................................... 159 In-App Navigation ..................................................................................................................................... 166 Utility Application ................................................................................................................................. 169 Utility Application Using Storyboard..................................................................................................... 178 Tab Bar Application ............................................................................................................................... 188 Tab Bar Application Using Storyboard .................................................................................................. 196 Page-Based Application ........................................................................................................................ 201 Modified Page-Based Application ......................................................................................................... 213 Navigation Controller ............................................................................................................................ 223 Navigation Controller With Storyboard ................................................................................................ 240 Split View / Master-Detail ..................................................................................................................... 246 I/O ............................................................................................................................................................. 262 iOS Application File System................................................................................................................... 264 Classes Supporting I/O .......................................................................................................................... 266 Data Classes .......................................................................................................................................... 267 Examples ............................................................................................................................................... 268 Preferences and Settings .......................................................................................................................... 286 Property Lists ........................................................................................................................................ 287 UIApplication and UIApplicationDelegate ............................................................................................ 288 In-App Preferences ............................................................................................................................... 289 Settings Bundle ..................................................................................................................................... 301 Animation.................................................................................................................................................. 315 Animation on IOS .................................................................................................................................. 319 Nicolaas tenBroek (AMDG) 3

iOS Development

Contents

UIView Animation ................................................................................................................................. 319 Core Animation ..................................................................................................................................... 323 Accessibility ............................................................................................................................................... 338 Accessibility Protocols........................................................................................................................... 342 Example App ......................................................................................................................................... 345 Sensing ...................................................................................................................................................... 385 Touch..................................................................................................................................................... 385 Motion Events ....................................................................................................................................... 395 Gestures ................................................................................................................................................ 398 Accelerometer....................................................................................................................................... 406 Core Motion .......................................................................................................................................... 414 Device Location ......................................................................................................................................... 429 Core Location ........................................................................................................................................ 430 Maps ..................................................................................................................................................... 449

Nicolaas tenBroek (AMDG)

iOS Development

Introduction

Introduction
This book was written to support the iOS side of the Mobile Application Development curriculum at Heartland Community College (Normal Illinois, USA). As such, it assumes a competence in Object Oriented Programming consistent with a current Computer Science I course. This book is not intended for students whose instruction was in a Structured Programming course or worse, in an Objects Last environment. If the reader does not fit into the assumed profile, then this book should be abandoned for a different one. Additionally, this book does not assume any familiarity with the Objective-C programming language, the XCode development environment, or the C programming language. While C experience is helpful and certainly required for anyone wishing to develop for iOS professionally, very little C is used in this book in consideration of its introductory nature. This book is provided free of charge and is available for any individual student or organisational use, though all copyrights are retained by the author and companies mentioned within the text. As such, it was written entirely without editorial support and may well contain abuses of the English language that are far beyond the pale and I apologise in advance for any errors or awkwardly worded phrases you encounter. All sample applications included in the text have been tested and were working at the time of publication. As is the nature of such things, certain features may cease to work as the OS is updated. I will endeavour to update the text to keep it relevant and the examples working. The updates will be available on my website. However, as this book is intended for classroom use, none of the code will be available for download. While this is not the norm for textbooks today, students learn almost nothing by simply downloading and running code. Any serious programmer knows that if you truly wish to learn, then you must put in the time at the keyboard. I will not deprive you of that opportunity by doing the work for you. Please contact me at Nicolaas.tenBroek@heartland.edu with any errors and I will update the text as quickly as possible.

Nicolaas tenBroek (AMDG)

iOS Development

Language Basics

Language Basics
This book is written for people who are already programmers and who would like to quickly get up to speed and become productive in iOS. This is not an Introduction to Programming book. Therefore we shall make the assumption that you have functional knowledge of an Object Oriented Programming language and are fully versed in the related concepts like encapsulation, implementation hiding, polymorphism, inheritance, and dynamic typing. We shall also assume knowledge of flow-control and iteration. If this is not the case for you, then you should really choose a different book, as those topics (and many others) will be covered for syntax only and not for proper usage techniques. Objective-C is a C derived language. While knowledge of C is not assumed in this book, if you plan to become a professional iOS developer, it would be well worth your while to learn that language as well. Many of the more advanced libraries for iOS are written in C, so a working knowledge of that language will make understanding those libraries easier. Thankfully, many of the elements of the C language have survived in Objective-C, so learning C later will be a bit easier. There are elements of other languages (like C++ and Smalltalk) that have been imported into Objective-C, but it is not directly related to those languages, so direct knowledge of those languages is not required. The relationship between ObjectiveC and C is so close that you will very often be programming in both languages at the same time, sometimes even within the same statement. This chapter will cover the primitive data types, operators, and flow control. As these items are fairly consistent across programming languages, they will mostly be familiar to you, but Objective-C adds in some of its own unique items. A short but careful examination is therefore warranted to ensure we do not skip over an important difference. We shall begin with data types. As Objective-C is derived from C, all of the C data types are available. Depending on your language background, you may be shocked to find that in C (and its derivatives) the bit-length of a variable in a given data type is itself variable. For instance, an int may be 16 or 32 bits depending on the target system of your application and the compiler settings you select. Luckily, we are focusing on iOS rather than all platforms that support Objective-C (like Mac OS or Linux), so we are able to make assumptions about the size of variables. It is important to keep in mind that the size assumptions will not extend to other platforms, or even all future iOS platforms.

Nicolaas tenBroek (AMDG)

iOS Development

Language Basics

Data Types
Type Name
short char int long long long

Data Type
integer integer integer integer integer

Size in Bits*
16 8 32 32 64

Range Of Values
-32,768 to 32,767 -128 to 127 -2,147,483,648 to 2,147,483,647 -2,147,483,648 to 2,147,483,647 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 ~ -3.4E+38 to ~ 3.4E+38 ~ -1.7E+308 to ~ 1.7E+308 ~ -1.7E+308 to ~ 1.7E+308 NO and YES NO has a value of 0 YES has a value of 1 N/A

float double long double BOOL

real real real integer

32 64 64 8

id

Object or Class reference

N/A

*Note that the sizes of these data types may be different in the simulator than on a real iOS device, as they will adopt the size used by the computer you are using to run the simulator. For instance, if you run the simulator on a 64-bit computer, the long long and long double in the simulator will be 128 bits in length. The sizes of variable on actual devices will also likely be larger in the future, as mobile processors will certainly increase in power. The short, int, long, and long long data types can all be modified with the keyword unsigned. Adding unsigned means the last bit will not be interpreted as the sign bit and therefore all the numbers will be positive. This has the effect of doubling the range of numbers on the positive side, with zero as the smallest number the variable can store. For instance, a variable declared as an unsigned short would be able to store numbers in the range 0 to 65535, rather than the -32768 to 32767 of the signed version.

Nicolaas tenBroek (AMDG)

iOS Development

Language Basics

Additional keywords for use with declaring variables are static, auto, const, and volatile. If the static keyword is used when declaring a local variable, it causes the variable to be created only once, the first time a method or function is called. From that point on the variable continues to exist, maintaining its value, while still retaining its status as a local variable. Here is an example: .... { //start of some method or function body static int useageCount = 0; //usageCount is only created the first time it is needed and continues to maintain //its value across subsequent calls to this method }

The auto keyword is the opposite of static and is applied automatically to all non-static local variables, so while you can use it, you never need to. Auto causes local variables to be created and destroyed with each method or function call. The keyword const is used to create constants. In C and C-derived languages a constant must be initialised at the time of its creation. For example: const double PI = 3.1415926535897932384626433832795; //sure its only an approximation, but then this is only an example The keyword volatile is functionally the opposite of const. It prevents compiler optimisation of code that accesses this variable. We use volatile any time the contents of the variable could change in an unpredictable manner. This is often the case in a multi-threaded environment where more than one thread could be changing the contents of the variable, and those changes could come from multiple methods or functions.

Nicolaas tenBroek (AMDG)

iOS Development

Language Basics

Operators
Most of the operators in Objective-C are common programming operators and work the same way as they do in other languages. The mathematical operators are:

Operator = + += -= * *= / /= ++ -% %= assignment addition

Function x = y;

Example

x = x + z; x += y; x = x - y; x -= y; x = x * y; x *= y; x = x / y; x /= y; x++; ++x; x--; --x; x = x % y; x %= y;

addition and assignment subtraction subtraction and assignment multiplication multiplication and assignment division division and assignment increment (post and pre) decrement (post and pre) mod mod and assignment

Nicolaas tenBroek (AMDG)

iOS Development The logical operators are:

Language Basics

Operator ! == != > >= < <= && || ?: NOT equal to NOT equal to greater than

Function !x x == y x != y x>y x >= y x<y x <= y

Example

greater than or equal to less than less than or equal to AND OR conditional (short if/else structure)

x > y && y > z x > y || y > z (example below)

The bitwise operators are:

Operator ~ & &= NOT AND

Function x = ~y;

Example

x = x & y; x &= y;

AND and assignment

Nicolaas tenBroek (AMDG)

10

iOS Development

Language Basics

Operator | |= ^ ^= >> >>= << <<= OR OR and assignment XOR

Function

Example x = x | y; x |= y; x = x ^ y; x ^= y; x = x >> 4; x >>= 4; x = x << 8; x <<= 8;

XOR and assignment right shift right shift and assignment left shift left shift and assignment

Finally, we have some other operators. Several of these operators have been re-purposed which means their actual job is entirely dependent on the context in which they are used. For instance, when the ampersand is used as a binary operator is a bitwise and. Used as a unary operator, the ampersand gives us the address of a variable in RAM.

Operator .

Function The dot operator is used to access the properties of an object or to access the members of a structure. Note: The dot operator is not used to call methods in Objective-C.

Example myObj.property = 6;

The comma operator forces evaluation of statements from left to right.

x++, y /= x;

Nicolaas tenBroek (AMDG)

11

iOS Development

Language Basics

Operator []

Function

Example

Used with C-style arrays the square brackets are array[i] = 44; used to indicate an index. [myObj someMethod]; Used with Objective-C objects the square brackets call methods. When used in a variable declaration the asterisk int *ptr; creates C-style pointers or Objective-C object MyClass *myObj; references. Returns the address of a variable. When used in variable or method declarations the minus indicates an instance item while the plus indicates a class item. &myVar - (void)method1; + (void)method2;

& +

Nicolaas tenBroek (AMDG)

12

iOS Development

Language Basics

Control Structures
The control structures in Objective-C will be familiar to almost everyone. We will start with the if and if/else constructs: if(condition) { //your code here } And if(condition) { //your code here } else { //your code here }

As well as the conditional statement: (condition)? true-section-code-here : false-section-code-here;

In some languages the conditions of these constructs would evaluate to a Boolean value. In C-derived languages the conditions must evaluate to an integer. If the integer is zero, the condition evaluates as false. Any other value (whether positive or negative) evaluates as true. When testing conditions, you must be aware that 0 is false and !0 is true. That means you cannot assume that a statement like (4 > 2) resolves to a value of one. You can only assume that (4 > 2) resolves to a value that is NOT zero. Additionally, the NULL or nil values used with pointers and reference variables can be treated as zero in a condition. Another common structure is the switch. Like the if structure and the conditional structure, the condition for a switch must evaluate to an integer. Each case label must be a single integer value. switch(condition) { case a: //your code here break;

Nicolaas tenBroek (AMDG)

13

iOS Development case b: //your code here break; ... case n: //your code here break; default: //your code here }

Language Basics

While the default case is optional, it is always encouraged. If you have accommodated all cases with the case statements, then include a default that logs an error message. An error logging default case will alert you to a missed a case or the data being out of the valid range for some reason.

Nicolaas tenBroek (AMDG)

14

iOS Development

Language Basics

Iteration
Objective-C provides four loops: do/while, while, for, and for-each. As is customary, all the loops iterate as long as the condition evaluates to true (NOT zero). The syntax for the do/while is: do { //your code here } while(condition); Note that the do/while is the only bottom checking loop and is the only loop that ends with a semicolon. The syntax for the while loop is: while(condition) { //your code here } The syntax for the for loop is: for(pre-loop; condition; post-loop) { //your code here } The pre-loop section is evaluated only one time. While it is generally used for creating loop control variables, it can contain any number of statements with additional statements being separated by commas. The post-loop section is evaluated only after the body of the loop completes (which means it will never be evaluated if the condition evaluates to false the first time it is checked). The post-loop section is generally used for manipulating the loop control variables, though it too may contain additional comma-separated statements. For example, the loop below will run ten times: for(int i = 0; i < 10; i++) { //do some work here }

The for-each loop is used for iterating over Objective-C arrays, sets, or dictionaries. Its syntax is: for(variable-declaration in data-set-name) { //your code here }

Nicolaas tenBroek (AMDG)

15

iOS Development

Language Basics

The variable-declaration is literally just that, a declaration of a new reference variable used only in the loop. The data-set-name is the name of an array, set, or dictionary. The loop works by populating your variable with a reference to one of the objects in the data set, then running the body of the loop. After the body runs, the variable is repopulated and the body run again. The loop guarantees that each item in the data set will be visited and that it will only be visited one time. Here is an example that processes a collection of strings: for(NSString *aString in ourCollection) { //do some work here }

While Objective-C uses C-style arrays for primitive values, it includes a class (NSArray and its derivatives) for creating arrays of objects. Keep in mind, the for-each loop works only on arrays of Objective-C objects, and not on the C-style arrays.

Nicolaas tenBroek (AMDG)

16

iOS Development

Language Basics

Preprocessor
C derived languages use an interesting subsystem called the preprocessor as part of the build process. The preprocessor works very much like a programmable text editor, in that it can be used to perform automated tasks on your code before it compiles. In a basic sense, the build process works like this: The pre-processor makes a temporary copy of your code That temporary copy is then edited according to the instructions you issued to the pre-processor The now-edited temporary copy is then handed to the compiler for compilation The compiler deletes the temporary copy after compilation completes (or fails)

The mechanism for issuing commands to the pre-processor is the # directive. There are a fair number of # directives, and while most are used quite infrequently a few are used constantly. In fact, you cannot write a single iOS app without using # directives. Fortunately they are quite simple items that carry out very straightforward tasks, making them both easy to understand and implement. Below is a nonexhaustive list covering only the directives you are most likely to need. Any C language reference can give you a complete list should you want to learn the more advanced preprocessor techniques.

Directive #include #import #define #if #ifdef #ifndef #else #elif #endif #pragma

Function Inserts the contents of a header file into a source file. Inserts the contents of a header file into a source file. Creates a symbolic constant or preprocessor macro. Allows testing the value of a symbolic constant to produce conditional compilation. Tests whether a symbolic constant has been defined. Used to produce conditional compilation. Tests whether a symbolic constant has not been defined. Used to produce conditional compilation Used in conditional compilation scenarios. Produces an else condition for #if, #ifdef, or #ifndef directives. Used in conditional compilation scenarios. Produces a secondary condition for #if, #ifdef, or #ifndef directives. Ends a conditional directive. Compiler defined. Used in XCode with the mark argument to help organise a source file.

The #include and #import directives are discussed in the following chapter, so we will defer commenting on those. We will demonstrate the #pragma directive later, and it requires little explanation, so we will defer that discussion as well. Symbolic Constants and Macros The #define directive is used very often to create symbolic constants, and can even be used to create macros. A macro is similar to a very simple method, and is useful in similar circumstances. For instance, if you have a computation that must be used multiple times you would usually create a method to house

Nicolaas tenBroek (AMDG)

17

iOS Development

Language Basics

the code. If the computation is simple enough (i.e. a single statement), then a preprocessor macro can be substituted for the method, which often results in faster code at run time. When used to create symbolic constants, the preprocessor executes a simple replace all feature, much as you would do with a standard text editor. The syntax is: #define CONSTANT_NAME constant_value For example, we may use this to define the limits of a boundary condition: #define MIN_VALUE 0 #define MAX_VALUE 1024 //Note that the use of upper-case letters in the names denotes //that these items are constants rather than variables.

Then, elsewhere in our code, we can refer to those symbolic constants as if they are real items. if((currentValue >= MIN_VALUE) && (currentValue <= MAX_VALUE)) { When the preprocessor sees the #define directive used this way it will search your code for the symbolic constant name and replace every instance with the constant value. So, after that process the compiler would see the following if statement: if((currentValue >= 0) && (currentValue <= 1024)) { The advantages of this approach are two-fold. First, if the value of the constant ever needs to be modified, you will only need to modify one line of code. Then a re-compile will reflect the change throughout your application. Obviously this approach is much less error-prone than a manual search and replace. The second advantage is that names make for easier to understand code than numbers do. By naming our symbolic constants with meaningful names our code will be easier to read and comprehend, which also means we will be less-likely to make mistakes. Before we go any further, we need a word of caution about #define. The preprocessor does not care what you use as the constant value. It is simply treating the situation as a search and replace operation. So, if you were to be a bit careless and end your directive with a semi-colon, that semi-colon would then be copied into your code. As an example, lets assume we made that mistake in our previous code: #define MIN_VALUE 0; #define MAX_VALUE 1024; . if((currentValue >= MIN_VALUE) && (currentValue <= MAX_VALUE)) { The preprocessor would faithfully replace the values, and the compiler would then see this statement: if((currentValue >= 0;) && (currentValue <= 1024;)) {

Nicolaas tenBroek (AMDG)

18

iOS Development

Language Basics

The compiler would then quite rightfully complain to you about the improper use of semi-colons. Actually, it would probably tell you that a right parenthesis was expected, or something along those lines. Of course, you do not see the temporary and edited file that the compiler sees, so it would indicate that the problem was in your code at the end of the symbolic constant. It may look something like this: if((currentValue >= MIN_VALUE) && (currentValue <= MAX_VALUE)) { ^ invalid semi-colon! Such an error will doubtless leave you perplexed, because you do not see a semi-colon, and you know that one does not belong in that position. Of course, the joys of such an error are unforgettable. You will likely make this type of mistake only one time. Using the #define directive to create macros expands on this search and replace behaviour by adding the ability to include arguments. The syntax is: #define macroName(argument_list) macro_value The macro name is treated like a method name, and typically follows the same naming conventions. The argument list is a comma-separated list of argument names (no data types) that will then be used in the macro value. The macro value is the computation, including the arguments from the argument list. For instance, we could create a macro to compute the circumference of a circle: #define PI 3.1415926535897932384626433832795 #define circumference(r) (2 * (r) * PI) float circ = circumference(someRadius); After the preprocessor runs, we would have the following line of code: float circ = (2 * (someRadis) * 3.1415926535897932384626433832795); The advantages of this approach may not be immediately obvious, but it is useful. First, it gives us the same benefits as using a method. Specifically, we do not need to re-type the same statement in multiple places, which means we have fewer opportunities for bugs. Second, by not truly using a method, we do not incur the overhead generated by a method call, so the code produced by the #define macro may actually run faster than using a method. Before we move on to the next item, lets take a close look at the syntax of the macro. You may well have looked at the macro and thought Hey, there are some extraneous parentheses in there; I can get rid of those. In fact, the parentheses are critically important when creating macros, and handle two different situations. First, we have no idea where exactly the macro will be used. It may well be part of a larger statement, used within some other computation. By enclosing the entire macro in a set of parentheses we ensure it is safe to use as a part of a larger statement. Second, we have no idea what

Nicolaas tenBroek (AMDG)

19

iOS Development

Language Basics

the arguments to the macro will be. The arguments could be a computation, or even some other preprocessor macro. By enclosing the arguments within a set of parentheses we ensure they will be treated as discrete units when the macro is expanded. For example, suppose we wrote the following code, leaving out the parentheses around the arguments: #define PI 3.1415926535897932384626433832795 #define circumference(r) (2 * r * PI) float circ = circumference(valueOne + valueTwo + valueThree);

The expanded code would then look like this: float circ = (2 * valueOne + valueTwo + valueThree * 3.1415926535897932384626433832795);

Obviously, that code needs the parentheses around valueOne + valueTwo + valueThree in order to come out the way we intended. Conditional Directives The conditional directives give us the ability to control what code is submitted to the compiler. They function in a similar fashion to standard programming conditionals in that they test to see if a condition is true. If the condition is true the if executes by including the surrounded code in the temp file to be handed to the compiler. If the condition is false the if is skipped, eliminating the code from the application entirely. If an else condition is used, then the compiled application will include one of the surrounded blocks of code, but not both. As an example, consider the following: #define MIN_VALUE 0 #define MAX_VALUE 1024 . .. .. #if (MAX_VALUE MIN_VALUE) > 2048 //code for large memory model here #else //code for small memory model here #endif

In the code above, as we change the values in our symbolic constants, we actually change what code is compiled.

Nicolaas tenBroek (AMDG)

20

iOS Development

Language Basics

The #ifdef directive can be used to check whether or not a symbolic constant has been defined. Conversely, the #ifndef directive can be used to test that a symbolic constant has not been defined. These are often used to assist in debugging, but must be used with great care. If you decide to use these directives to assist with debugging, you must ensure that the code that will be eliminated from the final application in no way changes any piece of data. One example of a safe use would be to remove lines that print out application state used during debugging. Objective-C includes a C-style function called NSLog() that is used to produce an application log that is similar (though not the same) to what you might print to a terminal. It is in some ways better than a standard print, because it always timestamps the lines it prints out. Additionally, in an iOS app, those prints are captured in the app log. When testing in XCode the log is displayed as the terminal window, so it serves a similar function during development as a print would. Of course, the last thing you want in a production app is lots of debugging printouts saved into a log file, which makes using NSLog() a bit unattractive. One approach to this problem would be to simply run through all of your code and comment out every NSLog() statement that contains debugging code, and leave in those that are reporting on real error conditions. This approach has many inherent dangers though. You may accidentally comment out a real error log; you may miss one (or more) of the debugging logs; or worse, you may accidentally comment out a regular piece of code. Obviously, the manual approach is fraught with danger and would be terribly annoying to implement as well. Either of those issues is a serious problem and would be cause enough to reject this approach as viable. The better approach is to use the conditional directives to do the work for us. Every XCode project contains a file named XXXX_Prefix.pch (the XXXX will be the name of the project), which is usually stored in a folder named Other Sources. Changes to the pch file affect all source code in the project, so this is a good place to create project-wide symbolic constants. We can add the code below to the pch file, and create a decent solution to our debugging problem. #define __DEBUG__ #ifdef __DEBUG__ #define dLog() NSLog(__VA_ARGS__) #else #define dLog() #endif

This code actually creates a #define macro called dLog(). The ellipsis in the argument list indicates that the macro accepts a variable number of arguments. The macro dLog then expands to produce the code NSLog(), also with a variable argument list (which is how NSLog is defined). In the case where __DEBUG__ has not been defined, then the macro dLog() produces no code at all, which effectively eliminates it from the application. To switch from debugging to production, all we would need to do

Nicolaas tenBroek (AMDG)

21

iOS Development

Language Basics

would be to comment out the statement #define __DEBUG__ (no need to delete it). Then a recompile would effectively delete all calls to dLog(). In our code we would continue to use calls to NSLog() for error conditions, and calls to dLog() for debugging statements. This approach minimises but does not eliminate the risk created by removing code. The automated nature makes this much less error prone than commenting out the code, but we still have one potential source of trouble. If any call to dLog() changes the value of a variable, we will have introduced a bug. It is critically important then that you ensure your debugging code never changes the values of variables. The preprocessor has more abilities than the few we have detailed here, many of which are useful in corporate environments using large and automated build processes. If you were working in such an environment, it would be well worth your time to investigate the preprocessor in depth.

Nicolaas tenBroek (AMDG)

22

iOS Development

Language Basics

Nicolaas tenBroek (AMDG)

23

iOS Development

Classes and Objects

Classes and Objects


Like all C-derived languages, Objective-C breaks its source code into two files: a header file and a source file. The header file has an extension of .h and is used to identify class and instance variables, properties, and method signatures. The source file has an extension of .m and contains the actual method definitions. Before reference variables or instances of any class can be created, and before methods of any class can be called, the header file for that class must be imported into the current file. To import a header file C uses the #include pre-processor directive, which has the following format: #include some_header_file_statement The header file statement needs to contain the name of the header file, and its location. If the header file is part of the standard library of files that ship with the compiler, we surround the file name with a set of angle brackets like this: #include <stdio.h> If the file is one we created in the current project, then we surround the file name with a set of quotes like this: #include MyHeader.h Objective-C introduces an additional methodology for header file inclusion through the directive #import. This new directive has some distinct advantages over the old #include directive, so you should use it exclusively when dealing with Objective-C libraries. If you import C or C++ libraries into your code, you will need to fall back on the #include directive. #import uses the same syntax as #include, so importing a built-in library will be done with angle brackets like this: #import <UIKit/UIKit.h> Whereas header files you write will use the quotes like this: #import MyHeader.h When the preprocessor encounters the #include and #import directives, it actually copies the contents of the imported file into the temporary copy of the current file. If both a header file and a source file issue the same #include directive, the preprocessors temp file will actually have two copies of the same code. This is obviously a very bad situation, so the author of every C header file was forced to include other preprocessor directives to prevent the multiple copies. Objective-C introduced the #import directive to solve this problem for us. The #import directive will automatically ensure that a header file is not included in the preprocessors temporary file more than one time. As we will not be writing the pre-processor directives used in C programming in our header files to prevent the multiple inclusion, it is important that you remember to use the #import for all Objective-C header files.

Nicolaas tenBroek (AMDG)

24

iOS Development

Classes and Objects

Classes
An Objective-C class is comprised of two parts: the interface and the implementation. The interface is typically in the header file and the implementation is in the source file. We will start by examining the interface. The interface tells us the class name, inheritance, and protocol implementation (if any). It also contains the class and instance variables, property statements, and method signatures. The format is: import_statements @interface class_name : super_class_name <optional protocol list> { access_specifier(s) class_and_instance_variable_declarations } property_statements class_and_instance_method_signatures @end

Here is a very simple example: Point2D.h


#import <Foundation/Foundation.h> @interface Point2D : NSObject { @private double x; double y; } @property (nonatomic, weak) double x; @property (nonatomic, weak) double y; - (double)distanceFromPoint:(Point2D *)anotherPoint; @end

Nicolaas tenBroek (AMDG)

25

iOS Development

Classes and Objects

Technically the property statements and method signatures can appear in any order, and can even be mixed. For your own sanity and that of your co-workers though, it would be best if you keep them separated and in consistent locations in all header files. The source file contains all the actual method definitions, so this is where all your work will go. The format is: import_statements @implementation class_name synthesis_of_properties method_definitions @end Here is an example source file, implementing the interface from earlier: Point2D.m
#import "Point2D.h" @implementation Point2D @synthesize x; @synthesize y; - (double)distanceFromPoint:(Point2D *)anotherPoint { return sqrt(pow(self.x - anotherPoint.x, 2) + pow(self.y - anotherPoint.y, 2)); } @end

Note that we began by importing our own header file. This is absolutely required. C-derived compilers will not link the source and header files together, so you must manually import all header files. In fact, the header file is never actually compiled. Only the source file makes it through to the compilation step, which is why the import is required at this stage. Do not worry about what is happening in this example, or what keywords are causing what actions. Right now you should only be focused on the structure of the header and source files. We will cover everything else momentarily.

Nicolaas tenBroek (AMDG)

26

iOS Development

Classes and Objects

The Self Reference


Every instance method has access to a reference called self. The self-reference refers to the object that was the receiver of the method call. With the single exception of assignment, self can be used as a standard object reference variable.

Nicolaas tenBroek (AMDG)

27

iOS Development

Classes and Objects

Access Specifiers
The access specifiers are: @private, @protected, @package, and @public. @private ensures that only the declaring class has access to the variable. While not the default setting, good Object Oriented Design practices tell us that most variables should be private, and should only be made more visible if there are no better mechanisms available (i.e. accessor methods). @protected is the default setting. It allows the declaring class and any subclass to access the variable. @package allows the declaring class, any subclasses, and any other classes in the same image to access the item. @public allows access by any class at any time. The syntax for using access specifiers is to indicate one specifier, and then declare all the variables with that level of access. The specifier is applied to all the variables that follow it until the end of the list is reached, or until another specifier is indicated. For example, consider the following partial interface:
#import <Foundation/Foundation.h> @interface Customer : NSObject { @private NSNumber *customerID; NSString *firstName; NSString *lastName; NSNumber *accountBalance; @protected NSString *address; NSString *phoneNumber; } @end

In the class Customer customerID, firstName, lastName, and accountBalance are all private and accessible only from within Customer. On the other hand, address and phoneNumber are protected and can be accessed and modified by Customer and any of its subclasses.

Nicolaas tenBroek (AMDG)

28

iOS Development

Classes and Objects

Properties
A property is basically an instance variable exposed through a setter and getter method. While it is entirely possible to write complete applications without using properties, their use can greatly simplify the task by automating the getter and setter accessor method code production. To create a property simply write the @property statement in the header file of a class and then the corresponding @synthesize statement in the source file, the compiler will handle the rest. Once a property has been created, it can be access via the dot operator as we did in the previous Point2D example with statements like anotherPoint.x. The compiler will examine any statement accessing a property and determine whether the getter or setter method needs to be used. In the example above, we were reading the values of the variables, so getters were used. With an assignment statement: self.x = 3.14159; the setter would be used. The compiler will literally replace those statements with method calls. You can also call the methods directly like this: [anotherPoint x] or [self setX:3.14159]; The format of the property declaration is:
@property (attribute list) data_type variable_name;

The attribute list informs the compilers generation of accessor methods and access rules via the dot operator. Valid attributes are:

Attribute Name getter = getter_name

Meaning Overrides the default getter naming scheme and uses the name specified. The default getter-naming scheme is to name the method with the exact name of the variable.

Nicolaas tenBroek (AMDG)

29

iOS Development

Classes and Objects

Attribute Name setter = setter_name

Meaning Overrides the default setter naming scheme and uses the name specified. The default setter naming scheme is to name the method set and then the variable name, with the first letter of the variable name upper-cased (i.e. a setter for a variable named x would be setX).

readwrite

Indicates both a setter and getter should be created. This is the default setting Indicates that a setter should not be created. Only a getter will be created, and any attempts to use the dot syntax to set the value of the variable will result in a compiler error message. Indicates that the new value passed to the setter will be stored using a simple assignment statement (using the assignment operator). This is the default attribute. The assign attribute was replaced by weak in iOS 5 and should only be used in apps that support the older versions of the OS or in projects that have disabled ARC. The weak attribute was introduced in iOS 5 and is similar to the assign attribute in action. This differs from assign in that it is supported by the Automatic Reference Counting process. Any reference property with a weak attribute is automatically set to nil when the object is deallocated.

readonly

assign

weak

Nicolaas tenBroek (AMDG)

30

iOS Development

Classes and Objects

Attribute Name retain

Meaning Indicates the new object passed to the setter should be retained. The previously existing object (if any) will be released. (See the memory handling section later in this chapter for a discussion of release and retain.) The retain attribute was replaced by strong in iOS 5 and should only be used in apps that support the older versions of the OS or in projects that have disabled ARC. This is the compliment to weak introduced in iOS 5. The strong attribute is functionally the same as retain, but works with the Automatic Reference Counting process. Indicates that a copy (or clone) of the passed in object should be created. The previous object (if any) will be released and the newly created object will be retained. Objects being copied must implement the NSCopying protocol.

strong

copy

nonatomic

Indicates that the accessor methods for this property are not thread-safe. By default accessor methods are atomic (though oddly there is no keyword for this). If you are not using the accessors in a multi-threaded environment they should be non-atomic. Atomic accessors incur a huge performance overhead that should be avoided if at all possible.

The @synthesize directive is used in the source file and instructs the compiler to automatically generate accessor methods according to the instructions provided by the @property directive. You have the option of writing the accessors yourself of course, and interestingly, you can even combine the approaches. For instance, if you need to run additional code when a property is set (i.e. data validation, Nicolaas tenBroek (AMDG) 31

iOS Development

Classes and Objects

or setting a flag), then you would need to write the setter method yourself. As long as your setter method follows the naming rules, the compiler will see your setter and generate only the getter method automatically. If you decide to write all the accessor methods yourself, then you can dispense with the @synthesize directive. While statements using the dot operator syntax are automatically rewritten with the getter and setter syntax when using properties, the dot operator can also be used to directly access instance variables that have not been exposed as properties. Accessing non-property instance variables via the dot operator generates direct access to the variable. You need to be aware of this rather confusing situation as it can cause unintended recursion or other issues. For instance, we could rewrite the previous Point2D example without using properties at all: Point2D.h
#import <Foundation/Foundation.h> @interface Point2D : NSObject { double x; double y; } - (double)distanceFromPoint:(Point2D *)anotherPoint; @end

Point2D.m
#import "Point2D.h" @implementation Point2D - (double)distanceFromPoint:(Point2D *)anotherPoint { return sqrt(pow(self.x - anotherPoint.x, 2) + pow(self.y - anotherPoint.y, 2)); } @end

While the source code in the distanceFromPoint: method is exactly the same as it was before, the resulting instructions will be very different. Rather than using the getters, this code will directly access the variables in the two objects (self and anotherPoint). This version is functionally the same as the previous one, but the implementation is quite different.

Nicolaas tenBroek (AMDG)

32

iOS Development

Classes and Objects

To see the actual consequences of this change, lets look at an example of overriding the compiler generated accessors. Ive decided to only accept positive values along the X-axis, so I will need to implement a setter method that ignores negative values. Here is our original header with @property directives again: Point2D.h
#import <Foundation/Foundation.h> @interface Point2D : NSObject { double x; double y; } @property (nonatomic, weak) double x; @property (nonatomic, weak) double y; - (double)distanceFromPoint:(Point2D *)anotherPoint; @end

Now, in our source file we will add the setter method for x, with the data validation: Point2D.m
#import "Point2D.h" @implementation Point2D @synthesize x; @synthesize y; - (void)setX:(double)newX { if(newX >= 0) { x = newX; } } - (double)distanceFromPoint:(Point2D *)anotherPoint { return sqrt(pow(self.x - anotherPoint.x, 2) + pow(self.y - anotherPoint.y, 2)); } @end

Nicolaas tenBroek (AMDG)

33

iOS Development Pay careful attention to the statement carrying out the assignment. It is: x = newX;

Classes and Objects

which is a standard assignment statement. Now consider what would happen if I had instead written: self.x = newX; In this statement the compiler would have substituted a call to the setter method for self.x. So the compiler would rewrite that line like this: [self setX:newX]; Which you have likely noticed is a recursive method call. Be very careful of this syntax when overriding the setter and getter accessor methods. We need one more word of caution here. If you do not write property statements in your header file, then the compiler will always treat the dot operator as direct variable access regardless of whether or not you have written accessor methods. The compiler only substitutes method calls for direct access when both the @property directive and the dot operator have been used.

Nicolaas tenBroek (AMDG)

34

iOS Development

Classes and Objects

Methods
Objective-C methods use a named argument approach. The advantage of this approach is that the method call indicates the purpose of each value being passed in, making it less likely that mistakes are made in method calls. The syntax may take some getting used to for programmers who have only used the comma-separated argument list, but it can make the method calls easier to understand when passing in numerous arguments. The format of a method signature is: scope (return_type)methodName:(data_type)variable nextArgument:(data_type)variable The scope is either instance or class. An instance scope is indicated with the minus sign, whereas a class scope is indicated with the plus sign. The return type can be any primitive data type, object reference, or void. The data type for an argument can be any primitive data type or object reference. All arguments are separated by spaces. This means you must be careful about your space usage when writing the method signature. A space in the wrong place can make the compiler think you are trying to start a new argument definition. Consider for a moment one of the methods in our previous example, which had the following signature:
- (double)distanceFromPoint:(Point2D *)anotherPoint;

We can tell from the minus sign that this is an instance method. The return type is double, and the method name is distanceFromPoint:. There is one argument: anotherPoint and it is a reference to a 2DPoint object. Note that the colon is considered part of the method name or argument name. It is required to separate the name from the data-type, but actually becomes part of the name. Lets examine a sample call of the method distanceFromPoint:. For this example assume we have two 2DPoint objects called pointOne and PointTwo. Our method call would look like this: double distance = [pointOne distanceFromPoint:pointTwo]; Note that the method is called by separating the method name from the receiver object with a space. Note also the square brackets surrounding the object and the method call. All of the arguments must be within the set of brackets. So, if we had a multi-argument method, each argument would be separated by a space and all of them would be within the set of square brackets. For example, consider this method from the class UIView: - (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview A sample call would look like this: [myView insertSubview:someView belowSubview:someOtherView]; Nicolaas tenBroek (AMDG) 35

iOS Development

Classes and Objects

Method calls may be nested if the return value from the inner method call is a valid object that can be used to call the outer method. This pattern is often used and results in lines of code which look like this: [[anObject getSomeValue] doSomethingWithValue]; Shadowing While shadowing of instance variable names is a common (and encouraged) practice in Object Oriented Programming, it does not work well in Objective-C and should be avoided. When you consider that instance data can be accessed directly by variable name or through the dot operator and that the dot operator syntax may or may not be replaced with a method call depending on whether a property has been created, you will understand that shadowing can cause real headaches. To help explain this, lets re-examine the setter method we wrote earlier: - (void)setX:(double)newX { if(newX >= 0) { x = newX; } } If we had used shadowing we would be left with: - (void)setX:(double)x { if(x >= 0) { x = x; } } Obviously the statement x = x; Is not what you might consider good. Worse, we cannot disambiguate the statement with the selfreference like this: self.x = x; because the compiler would generate a recursive call when presented with that kind of statement (as we discussed earlier). Your best option then is to use different variable names in your argument lists. A common practice is to precede the variable name with a or new or a similar designator. You should develop and follow a common naming convention within your organisation.

Nicolaas tenBroek (AMDG)

36

iOS Development

Classes and Objects

The init Method Each class needs at least one method called init. Any class that extends NSObject inherits this method. Most classes override this inherited method and also provide overloaded versions. The method init is typically called immediately after creating an object and has the job of a constructor. In fact, init must be called before any action at all is taken on the object. As it serves the role of constructor, init should set up all instance variables to ensure they are in a valid state before the object is used. The init method has a required return value of the receiver object used to call the method. As we do not really know the final type of the object (remember that the method will likely be called through inheritance), we use id as the return type. You must also remember to call the super class version of init in your own init. Regardless of whether you call the default or an overloaded version of the super class init, it must be the very first thing you do in your init method. In order to obey these rules you should follow this simple pattern in your init methods: if(self = [super init]) { //your setup code here } return self; We are required to check the return value from init as it may return nil if there was a failure of some sort when initialising the base object. As a demonstration of this pattern lets update our 2DPoint example. We will both override the inherited default init and provide an alternate version by overloading. Note that as the default init is inherited we do not need to specify its signature in the header file. Point2D.h
#import <Foundation/Foundation.h> @interface Point2D : NSObject { double x; double y; } @property (nonatomic, assign) double x; @property (nonatomic, assign) double y; - (id)initWithX:(int)newX andY:(int)newY; - (double)distanceFromPoint:(Point2D *)anotherPoint; @end

Nicolaas tenBroek (AMDG)

37

iOS Development

Classes and Objects

Point2D.m
#import "Point2D.h" @implementation Point2D @synthesize x; @synthesize y; - (id)init { if((self = [super init])) { x = 0; y = 0; } return self; } - (id)initWithX:(int)newX andY:(int)newY { if(self = [super init]) { self.x = newX; self.y = newY; } return self; } - (void)setX:(double)newX { if(newX >= 0) { x = newX; } } - (double)distanceFromPoint:(Point2D *)anotherPoint { return sqrt(pow(self.x - anotherPoint.x, 2) + pow(self.y - anotherPoint.y, 2)); } @end

You will likely have noticed that we accessed our x and y instance variables in different ways in the two init methods. That was simply to demonstrate that it is possible to directly access the variables and to use our setter methods from within the init. If our setter methods have data validation code in them, then it would be advisable to use them to avoid initialising an object with invalid data.

Nicolaas tenBroek (AMDG)

38

iOS Development

Classes and Objects

Please note that if you are using XCode version 4.0 or 4.1 the compiler will issue a syntax error for the if statement in the init method. Those two versions required (for some strange reason) an extra set of parentheses on that line like this: if((self = [super init])) { While not necessary from a coding standpoint and changes absolutely nothing about how that statement functions, those two older versions of the compiler would not continue without the second set of parentheses. Some programmers prefer to break that statement into two steps, which can also improve readability for novice programmers: self = [super init]; if(self) { Some even prefer to use the very descriptive: if(self != nil) { The method you choose is up to you as the differences amount to nothing more than style. However, if your company has a standard, then the best approach is to simply follow that standard. If the company does not have a standard, pick the method that makes the most sense to you and stick with it.

Nicolaas tenBroek (AMDG)

39

iOS Development

Classes and Objects

Blocks
In iOS version 4 Apple introduced a mechanism for defining what amounts to anonymous inner methods. They named these items blocks. Blocks can be stored in special block pointer variables and executed at any time in the future. In that way block pointers are similar to function pointers in C, but they are not exactly the same as the syntax has some subtle differences. Blocks also have the ability to access and modify variables that exist in the scope that defines the block, and given the ability to execute the block at a future time, you need to be a bit careful in regards to variable scope. After all, you do not want to accidentally attempt to access a variable that no longer exists. The syntax for a block pointer variable is much closer to the C style than it is to Objective-C and is as follows: return_type (^pointer_name) (argument_list) The return_type can be any valid method return type, which includes a primitive or an object reference. The pointer_name is any valid variable name, and the argument_list is a C-style comma-separated list of data types (note: not variable declarations). As an example lets consider a simple block that takes two integer arguments and returns the product of those arguments. The pointer for such a block would look like this: long (^mySimpleProductExample) (int, int); Note that if a code block takes no arguments we must specify (void) as its argument list, which is a rule inherited from C. Similarly, if the code block does not return a value its return type must also be void. An example of such a block pointer would look like this: void (^anotherBlockExample) (void); When declaring the block itself we simply use the carrot and argument list followed by a code block (i.e. a set of French Braces). In the argument list of the block declaration we fully declare the arguments with their data type and variable name. The full declaration for our product example from earlier would then look like this: long (^mySimpleProductExample) (int, int); mySimpleProductExample = ^(int a, int b) { return a * (long)b; }; The block can then be called later using the variable mySimpleProductExample. Again, we must use Cstyle syntax for the call rather than Objective-C syntax. A call to our example would look like this: long answer = mySimpleProductExample(1234, 5678);

Nicolaas tenBroek (AMDG)

40

iOS Development

Classes and Objects

Blocks are often used to create small sections of code that can be called after the method that creates the block has ended. For instance, when animating views, you can specify a block of code to be called when the animation completes. In the past you would have to specify a separate method for each animation completion. Blocks give us essentially the same thing, but without cluttering up the interface of our class. As blocks are relatively new, their usage is not widespread. Currently they are mostly used in animation and threading, but future iOS versions will likely use code blocks in many more places.

Nicolaas tenBroek (AMDG)

41

iOS Development

Classes and Objects

Objects
We have already seen some examples of using objects to call methods, so now it is time to examine their creation and handling. We create objects through an inherited class method called alloc. That should always immediately be followed by a call to one of the class init methods. For instance, we can create and initialise an instance of our 2DPoint class like this: 2DPoint *pointOne = [[2DPoint alloc] init]; If we would rather use the alternate init method it would look like this: 2DPoint *pointTwo = [[2DPoint alloc] initWithX:100 andY:360]; The alloc method dynamically allocates the memory required for the object and then returns an uninitialized object (using the data type id). We use that new object to call the init method immediately in order to avoid attempting to use an uninitialized object. Initialising an object prior to use is absolutely required, so this pattern of nested alloc and init calls is the recommended approach, though not required. While it is technically legal to break the two calls into separate statements, such an action puts your code at risk as other code could be inserted between the alloc and init statements later. By nesting the calls, we make it harder for other code to be inadvertently inserted between the steps. Memory Handling iOS does not yet include a key feature of modern operating systems, specifically a garbage collector. Apple left this subsystem out of iOS in consideration of both CPU and battery constraints. While it will likely be added in at some point in the future, for right now the lack of a garbage collector means you the programmer are the garbage collector. As all objects are dynamically allocated, you must take responsibility for indicating when objects are in use and when they are ready to be de-allocated. You must also carefully manage all reference variables to ensure that they only point to valid, existing objects or the value nil. Failure to carry out these steps will definitely result in memory leaks and possibly also application crashes (if for instance, you attempt to access an object that has been deallocated). To help you manage the memory, Apple created a simple system for indicating when an object is in use and when it is ready to be de-allocated. This is done through a simple counter called the retain counter. When an object is first created through the alloc+init process the retain counter for that instance is set to one. Each time an instance of a class indicates that it needs to store this object for later use the retain counter is incremented by one. When an instance of a class indicates it no longer needs to store this object for later use, the retain counter is decremented by one. When the retain counters value reaches zero the object is ready to be de-allocated. At that time the method - (void)dealloc is automatically called. After dealloc completes the object is destroyed with its memory returned to the system. To make this system work, we use two methods: -(id)retain and (oneway void)release. The method retain increments the retain counter, while the method release decrements the retain counter. Note that we must never explicitly call dealloc. That method will be called automatically after the retain

Nicolaas tenBroek (AMDG)

42

iOS Development

Classes and Objects

counter has reached zero. Calling dealloc manually will almost always lead to an app crash the next time an object tries to access the one that was de-allocated. (The specifier oneway indicates that the method can be called synchronously and the caller therefore does not need to wait for the method to complete before continuing on.) While the mechanics of this memory management system are simple to implement, you will have to decide the most appropriate time to actually use them. Most of the code you write will likely fall into one of just a few scenarios. We will cover those here, but it will be up to you to determine which one you are in at any given moment. iOS 5: Automatic Reference Counting In XCode 4.2 Apple introduced a new component of the compiler named the Automatic Reference Counter (abbreviated ARC). This component was specifically designed to work with iOS 5 (and we may presume later versions) and automates the retain/release cycle, making it much easier to manage memory. The ARC is in no way a garbage collector, instead it is employed at compile time to insert the appropriate retain and release method calls in an automated fashion. Automating the process reduces the chance of programmer-initiated mistakes and memory leaks, and should be considered for all new projects that will be using iOS 5 or later. ARC can be enabled when creating a new project by selecting the Use Automatic Reference Counting option in the project creation dialogue. Once it is enabled you need only to use the weak and strong attributes on property declarations and leave the rest up to the compiler. When ARC is in use you are forbidden from calling either retain or release directly, so those calls will generate errors. Any pre-existing projects or apps which support older versions of iOS will need to handle dynamic memory manually. The following scenarios will demonstrate proper memory-handling procedures for those situations when ARC has not been enabled. With the exception of creating an Autorelease Block (in scenario 5), the following code samples will cause build errors if ARC is in use. Scenario One: Short-term Objects You Create When you have created an object (i.e. via alloc+init), and you are planning to use the object only within a single block of code (i.e. a single method, if, or loop), then your situation is simple. As soon as you have completed your task, you release the object. For example:
- (void)someMethod { MyObject *obj = [[MyObject alloc] init]; //retain counter automatically set to 1 //code using obj here [obj release], obj = nil; //retain counter set to 0, and object reference discarded //objs dealloc method will be automatically called }

Nicolaas tenBroek (AMDG)

43

iOS Development

Classes and Objects

You will have noticed that the line of code which called the release method also contained an additional statement: obj = nil; While not required, this is good coding practice as it prevents you from attempting to use a reference to an object that has been released. Once release has been called you can no longer consider the object valid, and must not do anything else with it. The simple step of setting the reference to nil will prevent lots of bugs for you, and we strongly encourage this practice. Scenario Two: Long-term Objects You Create For this scenario let us consider how to handle objects we create, and need to continue using for an indefinite period of time. Specifically we will demonstrate how to handle instance variables that we create. For this example we will be required to override the inherited dealloc method. Demo.h
#import <Foundation/Foundation.h> @interface Demo : NSObject { @private MyObject *importantObject; } - (void)someWork; - (void)someMoreWork; @end

Demo.m
#import "Demo.h" @implementation Demo - (id)init { If(self = [super init]) { importantObject = [[MyObject alloc] init]; //retain counter set to 1 } return self; } - (void)someWork { //do some work [myObject aMethod]; //do some work }

Nicolaas tenBroek (AMDG)

44

iOS Development Demo.m


- (void)someMoreWork { //do some more work [myObject anotherMethod]; //do some more work }

Classes and Objects

- (void)dealloc { [myObject release], myObject = nil; //retain counter set to 0, and object reference discarded //myObjects dealloc method will be automatically called [super dealloc]; } @end

As the dealloc method is inherited, it is imperative that we call the super class version of dealloc so that it can properly release all of its dynamically allocated memory, and the current object. In the version of dealloc that you write, you will need to release all the objects you have created or retained and then call the super class dealloc after all that work has completed. The order is extremely important here as you need to ensure the current object is still viable while you are releasing the contained objects. Remember that dealloc is called automatically whenever the retain count of an object reaches zero. Therefore we never call dealloc directly. Your only responsibility is to release an object and then nil out its reference. Scenario Three: Long-term Objects Passed Into Your Method For this scenario let us consider a situation where we have a method that receives an existing object as an argument, and we need to maintain that object reference for later use. In this situation we are not the creators of the object. Demo.h
#import <Foundation/Foundation.h> @interface Demo : NSObject { @private MyObject *importantObject; } - (void)setImportantObject:(MyObject *)newImportantObject; - (void)someWork; - (void)someMoreWork; @end

Nicolaas tenBroek (AMDG)

45

iOS Development

Classes and Objects

Demo.m
#import "Demo.h" @implementation Demo - (id)init { if(self = [super init]) { //do initialization work here } return self; } - (void)setImportantObject:(MyObject *)newImportantObject { [importantObject release]; //retain counter of previous instance decremented by 1 importantObject = newImportantObject; [importantObject retain]; //retain counter incremented by 1 } - (void)someWork { //do some work [myObject aMethod]; //do some work } - (void)someMoreWork { //do some more work [myObject anotherMethod]; //do some more work } - (void)dealloc { [myObject release], myObject = nil; //retain counter decremented by 1 [super dealloc]; } @end

In this scenario you can see that the object has been passed in to the setter method. Notice that the first step we take is to release the object we are currently referencing. This is a required step to avoid memory leaks, as we are about to lose that reference on the next line. Notice also that our comments indicate only that the retain counter is being incremented and decremented. As we are not the creators

Nicolaas tenBroek (AMDG)

46

iOS Development

Classes and Objects

of this object, we really have no idea what else is happening to it outside of the current class. Many other objects could also have a reference to this object, so we cannot consider anything other than what we know at the moment. The three-step process demonstrated in our setImportantObject method is exactly the same as what synthesized accessor methods use if the retain attribute is used. Therefore, we could replace the current code with a property, and achieve the same result. Demo.h
#import <Foundation/Foundation.h> @interface Demo : NSObject { @private MyObject *importantObject; } @property (nonatomic, retain) MyObject *importantObject; - (void)someWork; - (void)someMoreWork; @end

Demo.m
#import "Demo.h" @implementation Demo @synthesize importantObject; - (id)init { if((self = [super init])) { //do initialisation work here } return self; } - (void)someWork { //do some work [myObject aMethod]; //do some work }

Nicolaas tenBroek (AMDG)

47

iOS Development Demo.m


- (void)someMoreWork { //do some more work [myObject anotherMethod]; //do some more work } - (void)dealloc { [myObject release], myObject = nil; //retain counter decremented by 1 [super dealloc]; } @end

Classes and Objects

Note the use of the retain attribute on the property directive causes exactly the same steps of release, assign, retain to happen as happened in the setter we manually wrote. This means we are still responsible for releasing the object in the dealloc method. In fact, one of the checks you can do to help alleviate memory leaks is to check the dealloc method release statements against the @property statements. Ensure that each retain attribute is matched to a release in dealloc. Scenario Four: Long-term Objects Received From Method Call This scenario is really only slightly different from the previous one. This time, we will receive the objects as a value returned from a method we are explicitly calling. The required behaviour is the same.
- (void)someMethod { //work here [importantObject release]; //release previous object importantObject = [someObject someMethod]; [importantObject retain]; //retain current object //work here }

If we had created a property with the retain attribute, the above code could be simplified to:
- (void)someMethod { //work here

Nicolaas tenBroek (AMDG)

48

iOS Development

Classes and Objects

self.importantObject = [someObject someMethod]; //release and retain automated //work here }

Of course, if you are not planning to keep this object long term (i.e. used only in the current method), then no special treatment is required.
- (void)someMethod { //work here MyObject *importantObject = [someObject someMethod]; //nothing special required [importantObject importantMethodCall]; //work here }

Scenario Five: Objects You Create and Return If you create an object within a method and then return the object, things get quite a bit more complicated. Consider this code fragment:
- (MyObject *)getMyObject { return [[MyObject alloc] init]; //memory leak!! }

In the method getMyObject we created and initialised a new instance of MyObject and then returned it to the caller. Unfortunately we have just created a memory leak. The leak occurs because we no longer have a reference to the MyObject instance and we therefore cannot release it. You may be tempted to solve the problem by releasing the object, but that invalidates it and will cause the caller of getMyObject to crash.
- (MyObject *)getMyObject { MyObject *obj = [[MyObject alloc] init]; [obj release]; return obj; //VERY BAD! INVALIDATES OBJECT BEFORE RETURNING! }

Nicolaas tenBroek (AMDG)

49

iOS Development

Classes and Objects

To solve this problem we need to examine another feature of Objective-C memory management: Autorelease Pools. An autorelease pool is a collection that we can use to store instances we are creating, but not managing. The method getMyObject that we were attempting to write is a perfect example of this scenario. We are creating an object, but are unable to determine its lifespan and are therefore unable to manage its memory. Autorelease pools work by simply keeping track of all the objects within them, and then, when the pool is drained (through a call to the drain method) iterating over the collection of objects and calling the release method on each of them. We can add our object to the current autorelease pool by calling the method autorelease, which is inherited from NSObject.
- (MyObject *)getMyObject { return [[[MyObject alloc] init] autorelease]; //proper technique }

While the returned object is now valid for the caller of getMyObject, we have introduced another problem. Our new object will hang around until the pool is drained. This could be well after all other objects are finished with it. Consider this scenario: We create an object and add it to the autorelease pool as we did above. Then the object is passed around to ten other objects, each of which store it for later use by calling retain. The retain counter is now 11 (one for the original allocation, and ten more for the objects using this one). All ten of those objects finish using our object and call release. The retain counter is now 1. The object therefore cannot be de-allocated. The retain counter will stay at one until the autorelease pool is drained, which could be a long time from now. Meanwhile, this now useless object is consuming resources and increasing the memory footprint of our application. You need to think very carefully before using this technique to ensure that objects do not live much longer than they are needed. The autorelease pool setup is really quite powerful, but also somewhat complicated. You can create a new pool anytime you wish (using the class NSAutoreleasePool), and then drain it when you are done. You may well want to do this in many places. For instance, if you have a loop that creates a large number of short-term objects, creating a pool before the loop and draining it after the loop is probably the best approach for managing the memory for those objects. In iOS, each thread is required to create its own pool, and XCode automatically creates one for the main thread running your app. This means there could be many pools in use at any given time. Each time an autorelease pool is created within a thread it is stacked on top of the previous pool from that thread (unless it is the first one in the thread of course). All calls to autorelease automatically add the object to the pool on the top of the stack. A pool will remain on the stack until it is drained. Creating and draining an autorelease pool is really quite simple:
NSAutoreleasePool *pool = [[NSAutoReleasePool alloc] init];

Nicolaas tenBroek (AMDG)

50

iOS Development

Classes and Objects

//do work here, adding objects to pool by calling autorelease method [pool drain], pool = nil;

Note that a call to drain also releases our pool, so we need to nil out our now invalid reference variable. Autorelease pools are handled a little bit differently when using the ARC. Rather than creating a pool, adding objects to it, and then draining it when done, we must use an autorelease block. Autorelease blocks are not limited to use with ARC, and in many situations result in more efficient code, but suffer from lack of finesse. When an autorelease block is employed, the pool is automatically created and drained, and all objects created within the block are added to it, regardless of whether or not the object should be in the pool. You must pay careful attention when creating objects within an autorelease block to ensure the pool does not contain objects that will persist longer than intended, or be released before their lifespan is complete. With the warnings duly issued, creating an autorelease block is even easier than creating a pool:
@autoreleasepool { //do work here objects automatically added to pool }

Strings and Numbers in Objective-C Strings in Objective-C are represented by instances of the class NSString and can be created two different ways. First, instances can be created from string literals by surrounding the string with a set of double quotes and preceding the entire statement with the @ character. For instance: NSString *aString = @This is a string.; Of course, as NSString is a class, we are also able to dynamically create instances through alloc and a handful of overloaded init methods. As with many other modern Object Oriented languages, strings in Objective-C are immutable, so any data provided through the init methods will be permanent data in the string. Interestingly, mutable strings can be created through a child of NSString called NSMutableString. The default init method returns an empty string, which can be useful if you need to prevent a nil reference, but have no data. The overloaded init methods allow us to create strings from raw bytes, C character arrays, the contents of local files, the contents of networked files (by providing a URL), and even other NSStrings. Perhaps one of the init methods you will find most useful allows you to format data for display to users. It is named initWithFormat: and takes an NSString as its first argument. That string will contain a list of

Nicolaas tenBroek (AMDG)

51

iOS Development

Classes and Objects

format specifiers that describe how to convert the other arguments. There are a large number of format specifiers, all of which are described in the String Programming Guide from Apple, so we will cover only the basic ones here and demonstrate their use.

Format Specifier %@

Purpose Insert an Objective-C object converted to a string through the objects descriptionWithLocale method if it is available, or description method if no locale is available. Insert a percent sign (no additional arguments needed) signed short, int, or long

%% %d %D %i %u %U %qi %qu %f

unsigned short, int, or long

signed long long unsigned long long float or double

The integer format specifiers can be modified ensure the number always takes at least a minimum number of spaces by inserting the minimum spaces between the % and the specifier character. For instance, using %5d ensures that the number will occupy at least five spaces. If the number is less than 5 digits long, it will be padded on the left with spaces (i.e. right justified). If you would like the number padded with zeroes rather than spaces, use %05d. The real number specifiers can be modified in a similar fashion to control both the minimum number of spaces occupied by the integral part and the exact number of digits represented by the fractional part. For instance, a format specifier of %4.2f indicates a minimum of four spaces before the decimal point

Nicolaas tenBroek (AMDG)

52

iOS Development

Classes and Objects

and exactly two digits after the decimal. Note that converting a floating-point number to a string can incur a rounding operation. Here is a short example of creating an NSString using the format specifiers: NSString *display = [[NSString alloc] initWithFormat:@X = %d, Y = %f, xVal, yVal]; While reading the NSString documentation you will likely notice that most of the init methods have class method counterparts. These class methods allow you to create an NSString without the allocation step. The strings are being allocated for you of course, and are added to the autorelease pool. Think carefully about the memory implications of either approach before using it. While the NSString format specifiers are useful for simple formatting needs, they will not be applicable in every situation. If you need more advanced number formatting, the NSNumberFormatter class would be an appropriate choice. Numbers When we need to represent numbers as objects in Objective-C we use instances of the NSNumber class. NSNumber instances can represent any of the numeric primitives and can convert data back to any of those types as well. You will primarily need to use NSNumber instances when collecting numeric data, as the collection classes are unable to store primitives. Collecting Objects Most every application will need to create a collection of objects at some point. Unfortunately Objective-C has only three primary means of collecting objects. They are NSArray, NSSet, and NSDictionary. All three classes are immutable, but have mutable children named NSMutableArray, NSMutableSet, and NSMutableDictionary. NSArray The NSArray class does exactly what its name implies. It stores data in an array, and allows you to access those data by index using the method objectAtIndex:. It also has methods for producing sorted arrays, sub arrays, and for performing basic file I/O with arrays. The NSMutableArray class can be used to do all those things and gives you additional methods for inserting, removing, and replacing objects. Like arrays in other languages you would use instances of NSArray when you need to organise data into an index-based list, and use the index to retrieve an item from the list. NSSet NSSet is a mechanism for storing unordered and unique objects. As a set it can contain each object only one time, so any attempts to store objects already in the set will be ignored. NSSet provides methods for converting the other collections to sets, enumeration, and creating subsets. The set is useful for collecting objects which do not need to be ordered and must be unique. If an order is required after creating the set, you can use the method sortedArrayUsingDescriptors: to produce a sorted array of unique items.

Nicolaas tenBroek (AMDG)

53

iOS Development

Classes and Objects

NSDictionary Dictionaries are used for storing key-value pairs of objects. This kind of mechanism allows for quick retrieval of data using only the key. While the data is not ordered by traditional standards, the keyvalue pairing allows for random data retrieval in much the same way as an index allows random data retrieval from an array. Dictionaries are very useful items for dealing with paired data. For example, imagine a scenario where we need the user to select a setting from a list of possibilities. We would probably represent the setting values as integers, but integers are a terrible thing to present to users, as they have little to no meaning for users. Instead we would present a list of descriptive terms to the user, but we need a way to link the terms to the numeric values. A dictionary can do just that. In this example the numeric data would be the values and the words would be the keys. So, we present the list of words to the user, they select one item from the list, and we use that selection as the key, asking the dictionary for the matching value. While in practice keys are often strings, any objects can serve as key or value. Memory Management and Collections Regardless of the collection in use, you must consider the memory management ramifications when storing data. Each Objective-C collection will retain objects that are added to it. The collection will then release those objects when they are removed from the collection or when the collection itself is deallocated. Obviously this behaviour is desirable and can make your life much easier, but you do need to be aware of the issues and handle your code carefully. Consider the following examples (all of which assume that ARC is not enabled).
MyObject *obj = [[MyObject alloc] init]; //retain count set to one [someArray addObject:obj]; //retain count incremented to two obj = nil; //MEMORY LEAK!

The proper code would be:


MyObject *obj = [[MyObject alloc] init]; //retain count set to one [someArray addObject:obj]; //retain count incremented to two [obj release], obj = nil; //retain count decremented to one

Note that the following code would also cause the same memory leak:
[somArray addObject:[[MyObject alloc] init]]; //MEMORY LEAK! Retain counter is two!

Nicolaas tenBroek (AMDG)

54

iOS Development

Classes and Objects

You must also be careful when removing objects as the collections release call will invalidate the object.
MyObject*obj = [someArray objectAtIndex:i]; //retain counter is one [someArray removeObjectAtIndex:i]; //releases object, retain counter is zero [obj someMethod]; //ERROR! Invalid Object Reference!

Memory management is very likely the biggest stumbling block for programmers who primarily use languages with automated garbage collection. The memory management in iOS is almost a worst-case scenario. We have no automated garbage collection, limited RAM, and a manual process for tracking object references. If Automatic Reference Counting is not available in a project, then it falls to you, and you must be vigilant in your object handling. You should develop a process to minimise damage wherever possible. For instance, every time you create a retained property, immediately add the release statement to the dealloc method. After compiling, go through your code and match every alloc and every retain statement with a release statement. Other practices like assigning nil to every released variable will also help keep you out of trouble. If you are using ARC, you should assign nil to a reference variable as soon as you are done with the object in order to minimise the memory footprint of your app.

Nicolaas tenBroek (AMDG)

55

iOS Development

Classes and Objects

Nicolaas tenBroek (AMDG)

56

iOS Development

Basic User Interface in iOS

Basic User Interface in iOS


Model View Controller
Most modern programming languages use a Model-View-Controller (MVC) architecture to manage and display user interfaces, and Objective-C is no different. The parts of the architecture actually refer to three separate objects all of which work together to create and manage the UI. The Model portion refers to the data that drives what will be displayed on the screen. The Model object is responsible for storing, organising, and validating the data. The View portion refers to the screen itself. This is the part the user sees and interacts with. The Controller portion is an object that manages communication between the Model and the View. When the View needs a piece of data to display, it requests that through the Controller. The Controller then passes the request to the Model, and returns the data to the View. When the View needs to modify the data (i.e. if the user changed something), it passes the new data to the Controller, which passes it to the Model. If the data update fails, the Controller must inform the View of the failure. While this setup may all seem a bit overly complicated, there is rationality in its design. By separating the three tasks we create a system that is more modularised and therefore easier to change. For instance, we might design the first version of our application to store all the data in a file and use an NSDictionary to manage that data at run time. If the next version of our application increases the amount and complexity of the data, we may decide to change our data storage to a database and a custom class for managing all the new data, replacing the dictionary. If our screen has been created with the modularised MVC architecture, this update could be carried out with a fairly simple change. We need only replace the old Model with a new one, with no changes to the View. If our original Model class was well designed so that the implementation was completely hidden, such a change could also be invisible to the Controller. From another perspective, imagine that you have developed your application for the iPhone and later decide to add a custom screen layout for the iPad. If your code is properly designed with a loose coupling between the Model, View, and Controller, you should be able to easily accomplish that change with a minimal amount of code. At best you will need only to create a new View, at worst a new View and a new Controller, while the Model remains stable. The biggest goal of the MVC architecture is future proofing. If you are careful about following the rules of OOP, and truly consider the MVC design when creating your apps, then future modifications will be faster, easier, and less error-prone. Additionally, the separation of tasks will encourage you to think about only the job at hand, which will drastically reduce the chance for bugs, and speed development. From an implementation perspective this means that while writing the Model, you should never consider the View. How the data will be presented to the user is completely irrelevant to the Model. Remember that properly modularised the Model should be able to power several completely different views (assuming they all want access to the same data), with no modification to the Model code. In a similar fashion, the View should never care where the data is going or how it is to be validated. The Views only concern is how to present the data in a meaningful and useful way to the user. The Controllers role is to be the mediator between these two classes. In the ideal world the Controller is the only object that knows both of the other objects exist (i.e. the Model and View are unaware of each

Nicolaas tenBroek (AMDG)

57

iOS Development

Basic User Interface in iOS

other), but it only knows enough of both to be able to shuttle data back and forth. Unfortunately in iOS this is also the place where the architecture breaks down a bit. In the perfect world, the Controller is completely unaware of how the View is presenting user data or responding to user-generated events. In Objective-C that line is not so clearly drawn. Changes to the type of components used in the View will often necessitate a change to the Controller. This is not an issue unique to Objective-C. In fact, in many languages the View and Controller have become fairly closely linked due to underlying data typing issues in the language. In implementation this means you the programmer must be extra vigilant to keep those intimate connections between the Controller and the View to a minimum. One small word of warning: Apple names its Controllers and its Views ViewControllers. This does not imply that the View and Controller have been combined; it is simply a poor choice of names on Apples part as it confuses the issue. The xxx_ViewController.m files you see in your application are the Controllers where as the xxx_ViewController.xib files are the Views. The View (as represented by the .xib file) is a special kind of file and is usually handled through a part of XCode called the Interface Builder.

Nicolaas tenBroek (AMDG)

58

iOS Development

Basic User Interface in iOS

View and Controller Life Processes


Each time a View and Controller combination is loaded into memory, presented to the user, removed from presentation, or unloaded from memory, a very specific sequence of methods is called. These methods allow you opportunities to carry out whatever tasks you deem necessary at each point along the process. Of course, you will not always need to override these methods, but there will be times when you need one or more of them, and understanding when in the process they are called is critical to your codes success. We shall begin by examining the initialisation process. The View-Controller setup imposes some interesting situations on you that have implications in how you initialise your objects. If a View and Controller pair is automatically created and loaded for you (i.e. you do not call alloc+init in your code), then the Controllers init method used will be: - (id)initWithCoder:(NSCoder *)decoder If you plan to carry out some custom initialisation you will need to override that method and call the super class version. If you create and load the View and Controller manually (i.e. in code using alloc+init), then you must call the following init method: - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle Obviously, if you have a situation where the View Controller pair will be loaded automatically in some places and manually in others, you will need to override both methods. In such a case it would be best to create a private method containing the custom initialisation code and call it from both init methods. After the Controller has been full created and the View loaded into memory, the next automatically called method called is: - (void)viewDidLoad This method call informs you that the View has been created and is ready for processing. It is typically used to carry out any initialisation of the graphical components that might be necessary. This method has a sibling method that is called when a View is unloaded from memory. That method is: - (void)viewDidUnload The viewDidUnload method should be handled in a similar fashion to dealloc. Any objects retained in viewDidLoad, and any IBOutlet properties (more on those later), should be released in viewDidUnload. All other retained objects would continue to be released in the dealloc method, so you will very often need both. Each time a view is presented to the user (i.e. each time it appears on the screen), two methods are called: - (void)viewWillAppear:(BOOL)animated - (void)viewDidAppear:(BOOL)animated

Nicolaas tenBroek (AMDG)

59

iOS Development

Basic User Interface in iOS

As is likely obvious from their names, viewWillAppear: is called right before the view is presented, and viewDidAppear: is called immediately after the presentation. You can use these methods as necessary to handle last-minute adjustments to graphical controls and displays. In a similar fashion, each time a view is replaced by another view a different pair of methods is called: - (void)viewWillDisappear:(BOOL)animated - (void)viewDidDisappear:(BOOL)animated These methods function in a similar fashion to the appear methods and have similar purposes. Note that unlike the init, load, and unload methods, the appear and disappear methods can be called many times during the life of a view. iOS 5 introduced a new feature called the Storyboard which controls the transition between an apps views. We will see more about the Storyboard shortly, but with its introduction came a new method in the life process of a controller: - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender This method is called by the Storyboard before the transition from one screen to another and allows an opportunity to setup the new screen. Using the segue parameter you can gain access to the source and destination controllers, while the sender parameter gives you access to the UI component (if any) which caused the transition to happen. If your app has multiple mechanisms for transitioning between the two screens then the sender parameter will help you understand where the transition was initiated. If you decide to override any of the methods listed here (and you often will), you must remember to call the super class version of that method. The super class will often carry out necessary functions in these methods, and failing to pass the calls up the chain of objects could make your app unstable.

Nicolaas tenBroek (AMDG)

60

iOS Development

Basic User Interface in iOS

Interface Builder
All programming environments have their share of jargon, and the combination of XCode, iOS, and Objective-C offers us plenty of room for adding more to the mix. For instance, the application that allows us to graphically edit the user interface is called Interface Builder (though at times it is also referred to as UI Builder or simply: IB). In previous versions of XCode Interface Builder was a separate stand-alone application, but in XCode version four Interface Builder was integrated more tightly into the IDE and now automatically loads in the code editing window. Interface Builders job is to allow you to create screens by dragging and dropping the components into position. IB then allows you to manage virtually all the visible and behavioural properties of the components to ensure things are set up exactly as you would like them. Finally, IB provides a graphical means to connect your user interface objects to your code, a process usually referred to as wiring up the components. In iOS applications the View is stored in a binary file called a nib file, so named because .nib was the extension to the file. Starting in Interface Builder version 3 the extension changed to .xib, but the files are still called nib files. The letter change indicates that these files are now created in XML, which means you can also easily edit them in a text or XML editor should you desire to do so. There will be no need to edit them manually at this point though as Interface Builder will do everything we need. The introduction of the Storyboard in iOS 5 brought a new way to manage the different screens within an app and their relationship to each other. When using the Storyboard most or possibly all the views will be stored in a single file with the extension .storyboard. The .storyboard file continues to use XML, but offers a better view of the different screens and their relationship to each other than is available when using the individual nib files. Unfortunately it is not backwards compatible, so you can only use it for apps supporting iOS 5.x and later. Even when using the Storyboard, you will often find it necessary to have some views outside of the .storyboard, so it is imperative that you understand both the individual .nib and the more complex .storyboard. To demonstrate both how to use Interface Builder and how to use the UIComponents, lets create a simple view-Based application. Under the File menu choose New and then New Project. Choose the Single View Application template:

Nicolaas tenBroek (AMDG)

61

iOS Development

Basic User Interface in iOS

Name the project HelloWorld by entering that in the Product Name field. Select iPhone as the Device Family, ensure that Use Storyboard is not checked, Use Automatic Reference Counting is checked, and Include Unit Tests is not checked:

Pressing Next will allow you to choose where to save the project. The project will be saved in a folder named with the Product Name, so pick an appropriate place for the folder and press Create. When the project has been saved you will be presented a screen similar to this:

Nicolaas tenBroek (AMDG)

62

iOS Development

Basic User Interface in iOS

The first screen is a bit overwhelming, but we do not actually need to change any of the defaults yet. Rather we are more interested in the files shown under the HelloWorld folder. Specifically we are interested in ViewController.xib, so select that file by single-clicking it.

When you select a nib file from your file list, the editor window will change to display Interface Builder.

There are really three main sections to the Interface Builder window. The leftmost section contains information about the ViewController and the list of components. The centre section shows the View just as it will appear on the devices screen. The rightmost section actually contains a large number of items, which we will treat in turn. If the rightmost section is not visible, press the Utilities button on the toolbar at the top right side of XCodes window:

Nicolaas tenBroek (AMDG)

63

iOS Development

Basic User Interface in iOS

Upon closer inspection of the left side pane you will notice three items. The top component (it looks like a translucent yellow cube) is called Files Owner and is the connection to the Controller for this View. The middle component is something called the First Responder and indicates the component that will receive user input. The last item looks like a grey and white square and represents the View itself.

After components have been added to the View, they will also appear on this list, though nested below the view. All Views are constructed of layers of components, so you will often collapse and expand the layers as your setups become more complex. When layers exist you will see a small disclosure button on the left side of the View like this:

If you press the disclosure button, the View will expand into a list that gives you access to all the components. Here is an example (again, this is only available after controls have been added to the View):

Lets look a bit closer at the View in the centre section of the screen. At the moment it is a large expanse of boring grey. We will remedy that soon, but before that happens, look at the top of the view. We appear to have a light grey bar with a green charging battery icon. This isnt actually part of the view. Rather it is here to help us with our layout by taking up the space where the real devices status

Nicolaas tenBroek (AMDG)

64

iOS Development

Basic User Interface in iOS

bar would be. If our application needs the entire screen we can turn this simulated status bar off. There are other simulated metrics that can be displayed to help us with our layouts, but the status bar is the one that is on by default.

Next, lets shift our focus to the Utilities pane on the right side of the window. The Utilities pane is actually made up of two sections, one on the top and one on the bottom. The sections each have a control bar at the top with a set of buttons that govern what you will see in the section. We will begin our examination of the Utilities pane at the top. You will notice six buttons on the top control bar.

Each of these buttons will display a different inspector. The inspectors reflect information about the currently selected item within the view. So, as you choose different controls the kinds of information available will change to reflect only the appropriate items for that kind of control. The first button is for the File Inspector and gives us information about the file on disk. You will not often need this utility, but it is handy if you need to use the Finder to manage files manually. The second button lists a Quick Help pane that gives you a summary of the particular element you have selected in the view. The third button shows us the Identity Inspector and gives us information about the class of the item (you can change this if you create custom versions of the controls) and gives us a place to help control the Accessibility features. iOS is an incredibly friendly system for the blind, and you should seriously consider enabling as much accessibility as possible. We will go into more detail on how to support

Nicolaas tenBroek (AMDG)

65

iOS Development

Basic User Interface in iOS

accessibility in a later chapter. Here is a screenshot of the Identity Inspector after selecting a label component:

Next, lets look at the Attributes Inspector. This inspector gives you access to virtually all of the configurable properties that control appearance and behaviour. Again, keep in mind that the properties available will change based on the control you have selected. In the example below we have again selected a label component.

The penultimate button on the toolbar gives us access to the Size Inspector. This inspector allows us to control the size and placement of the control by entering numeric values rather than dragging and dropping. This can be handy when you need to be sure that controls are at specific pixel locations on the screen or that controls are always a specific distance apart. We will see an alternative placement

Nicolaas tenBroek (AMDG)

66

iOS Development

Basic User Interface in iOS

mechanism that shows the recommended values a bit later. You can also use this inspector to indicate how the selected control will respond to changes in the screen size (i.e. when the device is rotated).

The last button on this toolbar brings up the Connections Inspector. This inspector allows us to see what kinds of connections we have created between our code and the selected control. The connections can also be modified from this screen (this is the wiring up we mentioned earlier). While this screen can be handy, very soon we will see an alternative (and more direct) approach to this information. At the moment the selected control hasnt been wired to anything, so this screen is a bit bare.

The bottom portion of the Utilities pane contains several libraries: file templates, code snippets, objects, and media. Here we have selected the Object Library for display. The Object Library gives us access to the UI objects we can add to our view.

Nicolaas tenBroek (AMDG)

67

iOS Development

Basic User Interface in iOS

Lets add a label to our view. We do this by selecting the Label component from the list and dragging it onto the view. As you drag components around your view Interface Builder will display blue lines to help you position your component on the screen. In the screenshot below you can see a top margin line and a vertical centre line. The lines that appear near the edges of the screen indicate the suggested margin size. All touch screens lose accuracy for detecting touches around the edges of the screen, so positioning controls right up against the edge of the screen may make them less responsive. Of course, the margins also help to frame your application, providing a more consistent look. You certainly may position your controls at the edge, but be aware that small controls may not respond well in that position.

Nicolaas tenBroek (AMDG)

68

iOS Development

Basic User Interface in iOS

Once you have the label where you would like it, simply release it. By default the label just displays the word Label which is not terribly helpful. Double-click on the word and you can type in something new. The label will expand to contain the text you type:

Press enter to complete the editing process. Of course, now that the size of the label has changed it is no longer centred on the screen, so you may want to move it again. Now lets add a second label. As we drag this new label close to the first one we again see the blue margin line, and a new line appears between the two labels. This is the suggested separation between controls. You can use this simply to keep the spacing consistent, but it also informs us of the minimum distance necessary for touch accuracy. While it is possible to position controls immediately adjacent to one another or even overlapping, accuracy in detecting the users touch will be lost leading to unpredictable behaviour and frustrated users.

Next lets add a text field to our view. Begin by selecting the text field from the list:

And drag it into place:

By default the text field is pretty small, so lets expand it. Click on the light blue dot on the right side of the text field and drag it over to the right side of the screen:

Nicolaas tenBroek (AMDG)

69

iOS Development

Basic User Interface in iOS

Not all controls are re-sizable, and some can only be sized on one of the two dimensions. The blue dots inform you of the changes that are possible. We can see in our text field that the resizing dots are only present on the left and right sides. This informs us that text field components can only be resized along the horizontal axis. Now that we have built a basic screen, we can add some code to use the components. Component interaction in iOS can be handled through outlets, actions, and delegates. We shall look at the outlet process first.

Nicolaas tenBroek (AMDG)

70

iOS Development

Basic User Interface in iOS

IBOutlet
By tagging properties with the keyword IBOutlet we can expose them to Interface Builder. It is a fairly straightforward process to make an IBOutlet property, simply create a property of a UI type, add the word IBOutlet and then save the header file. Interface Builder will then have access to the property. Wire up the property, and when the nib file is loaded at runtime, the properties will be set to valid instances of UI objects. You need only create IBOutlet properties for elements you want to access while your application is running. For instance, if a labels contents will not change during the execution of the application, then there is no need for a property referencing that label. If the user is typing in a text field, then a property would be needed in order for us to be able to retrieve the text. To demonstrate this process we shall create a property that will connect to the text field we added to our view, wire up the property, and then retrieve its contents in code. We begin in the header file for the controller. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController //create a property this way if not using ARC, //and don't forget to "release" it in the viewDidUnload method. //@property (nonatomic, retain) IBOutlet UITextField *nameTextField; //create a property this way if using ARC @property (nonatomic, weak) IBOutlet UITextField *nameTextField; //in either case, don't forget to synthesize the property @end

Now that we have created the property, lets move to the controllers source file to write the code demonstrating the use of that property. ViewController.m
#import "ViewController.h" @implementation ViewController @synthesize nameTextField; - (void)someRandomMethod { NSLog(@"Contents of text field: %@", self.nameTextField.text); } // More methods here - removed for brevity

Nicolaas tenBroek (AMDG)

71

iOS Development ViewController.m


#pragma mark - View lifecycle - (void)viewDidUnload { //use the line below if not using ARC //self.nameTextField = nil; [super viewDidUnload]; } /* You will need this method if you are not using ARC - (void)dealloc { [super dealloc]; } */ @end

Basic User Interface in iOS

In our code sample we synthesized the property just as we would for any property, and added some commented code to demonstrate how to properly handle the property if not using ARC. Remember that when using properties, an assignment statement is rewritten to call the setter method, so the line of code: self.property_name = nil; Is functionally the same as this line: [property_name release], property_name = nil; Note that as we mentioned before, IBOutlet properties should be released in viewDidUnload rather than dealloc. Before we go much further we need a quick note about accessing IBOutlet properties. These properties are usually not set until after the super version of viewDidLoad has completed. Therefore any code written in viewDidLoad that uses properties will need to execute after the call to the super class version. Also remember that while viewDidLoad runs only one time, viewWillAppear: and viewDidAppear: are both run every time a view is displayed. If the control needs to be set up only one time and then retain its value, use viewDidLoad, otherwise use either viewWillAppear: or viewDidAppear:. Regardless of the method you use, you must remember to call the super class version of the method as well.

Nicolaas tenBroek (AMDG)

72

iOS Development

Basic User Interface in iOS

Below is an example of using viewWillAppear: to set a default value in the text field:
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.nameTextField.text = @"John Doe"; }

Now that we have some code, we can wire up our property. Save your work and then switch back to Interface Builder by selecting the nib file. We will then start the wiring up process by right clicking on Files Owner.

You will recall that Files Owner represents the View Controller, so that object has all of our properties and code in it. In the popup context menu you will likely notice that the View property from our MVC architecture is already present and connected.

Here you can see that our property nameTextField appears in the list of outlets. The property will appear in this list immediately after it has been declared in the Controllers header file (well, immediately after the file has been saved). The empty circle on the right side of the pop-up indicates that this property is not connected to anything. To wire up the property click in the empty circle and drag over to the text field we added earlier.

When you have dragged the line over a valid control the control will flash and highlight itself in blue.

Nicolaas tenBroek (AMDG)

73

iOS Development

Basic User Interface in iOS

Drop the line when you have reached the text field and the Files Owner dialog will change its display to indicate the connection.

While we have the context menu pop-up open, lets examine a couple of other things it can do. If your view has a large number of controls, and you arent sure which outlet is wired to which control, simply move your mouse over the connection. The control will highlight itself just as it did when we formed the connection. Next, notice the small x next to the control in the connection? If you press that it will de-wire the connection and you will again have the empty circle. Finally, if the pop-up is covering up a control you need to access, you can move it out of the way. Simply grab the title portion (at the top) and drag it away. Unlike other pop-ups this one will remain open until pressing the x button on the top-left explicitly closes it.

Nicolaas tenBroek (AMDG)

74

iOS Development

Basic User Interface in iOS

IBAction
While the IBOutlet is very good mechanism allowing us to access a control at runtime, it does nothing to help us know when the user has interacted with a control. One of the methods for detecting user activity is the IBAction. IBActions are simply methods that we can wire to controls. The controls then call these methods any time the indicated action occurs. The names of IBAction methods are entirely up to you, but the method signature must conform to one of following three patterns: - (IBAction)methodName; - (IBAction)methodName:(id)sender; - (IBAction)methodName:(id)sender event:(UIEvent *)event; The selection of signature is also entirely up to you. If you are writing a method that is only called by one control, then the first signature template will work perfectly. If many controls are calling your method and you need to know which control it was for some reason, you should use the second template. The third signature also gives you a reference to the control that called the method and adds an event object that encapsulates additional information about the event like the type of event and a timestamp. Keep in mind that properly designed methods will do the same thing regardless of the object that called the method. You should not design a method that does one thing if called by control A, and something else if called by control B. That situation calls for two separate methods. To demonstrate IBAction methods, lets add an action to our previous code and connect it to a button on the screen. When the user presses the button we will display their name in a dialogue. To begin we need to add the new code to both the header file and the .m file. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController //create a property this way if not using ARC, //and don't forget to "release" it in the viewDidUnload method. //@property (nonatomic, retain) IBOutlet UITextField *nameTextField; //create a property this way if using ARC @property (nonatomic, weak) IBOutlet UITextField *nameTextField; //in either case, don't forget to synthesize the property - (IBAction)sayHello; @end

Nicolaas tenBroek (AMDG)

75

iOS Development

Basic User Interface in iOS

Notice that IBAction methods are placed in the same location as regular methods in the header file. In practice these are regular methods, but with a special signature. You may well wish to group your action methods together for your own sanity and organisation, but that choice is up to you. Next, we will add the action method to the .m file. ViewController.m
#import "ViewController.h" @implementation ViewController @synthesize nameTextField; - (IBAction)sayHello { UIAlertView *hello = [[UIAlertView alloc] initWithTitle:@"Hello!" message:[NSString stringWithFormat:@"Hello %@!", self.nameTextField.text] delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [hello show]; //if not using ARC: //[hello release], hello = nil; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. } - (void)viewDidUnload { [super viewDidUnload]; //use the line below if not using ARC //self.nameTextField = nil; } /* You will need this method if you are not using ARC - (void)dealloc { [super dealloc]; } */ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.nameTextField.text = @"John Doe"; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; }

Nicolaas tenBroek (AMDG)

76

iOS Development ViewController.m


@end

Basic User Interface in iOS

Notice in the sayHello method we create something called a UIAlertView. The UIAlertView class is used to create a dialogue that will temporarily overlay the screen, and allow the user to respond. The dialogue has a configurable title, can display a fairly long message, and has completely customisable buttons. You must always provide one button title, and can provide more if you need them. You can see from the init methods arguments that it also requires something called a delegate. We will cover delegates momentarily, so we will leave that discussion until then. In UIAlertView the delegate only comes into play if more than one button is indicated. At the moment we are using only one button (labelled Close), so the delegate will go unused and can be ignored. Just as with the IBOutlet, when an IBAction method prototype has been added to the header file and the file has been saved, the action will become available to Interface Builder. We can then add a button to our screen. Begin by selecting the Round Rect Button from the object list:

Drag the button onto the screen and place it wherever you would like:

Next, edit the buttons label:

Nicolaas tenBroek (AMDG)

77

iOS Development

Basic User Interface in iOS

With the button created, we can wire it up to the action method. As before, we begin by right-clicking on Files Owner to bring up the connection dialogue. You will notice that there is now a new section in the dialogue labelled Received Actions and our IBAction method is listed there:

Next, wire up the action to the button by dragging from the actions circle to the button:

When you release the mouse button you will notice a new dialogue popping up. This one lists all the actions that the UIButton can respond to. We will use Touch Up Inside for this action so select that one:

Nicolaas tenBroek (AMDG)

78

iOS Development

Basic User Interface in iOS

Thats it! Save, build, and run. When the simulator starts up, type your name in the text field and press the Say Hi button:

When you release the button the UIAlertView dialogue will appear:

Nicolaas tenBroek (AMDG)

79

iOS Development

Basic User Interface in iOS

Delegates
While IBActions are methods that can be linked actions caused directly by the user interacting with the screen, there is another class of events that are generated through other means. To capture those events Objective-C uses a delegate pattern. A delegate is simply a class that implements a given protocol. We link the instance of that delegate class to another object, which generates the events via a property called delegate. Whenever an indicated event occurs the object notifies the delegate about the event. The delegate pattern is used much more widely than the IBAction pattern, and can be easily implemented entirely in code, or if you prefer, it can also be wired up through Interface Builder. The UITextField we used for the name provides a good opportunity for an example of delegate usage. The way our application works at the moment, if you tap in the text field to type your name, the keyboard pops up, but once available it never disappears. We can remedy this situation by implementing the UITextFieldDelegate protocol in our code and then wiring up Files Owner as the delegate of the text field. The UITextFieldDelegate protocol allows us to implement methods to be notified of editing events beginning and ending with textFieldDidBeginEditing: and textFieldDidEndEditing:. It also allows methods which will ask our permission before editing like textFieldShouldBeginEditing: textFieldShouldEndEditing:. In our sample we will implement the method textFieldShouldReturn: which is called whenever the user presses the return button on the keyboard. We will begin again by modifying our header-file with the new code in bold. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITextFieldDelegate> //create a property this way if not using ARC, //and don't forget to "release" it in the viewDidUnload method. //@property (nonatomic, retain) IBOutlet UITextField *nameTextField; //create a property this way if using ARC @property (nonatomic, weak) IBOutlet UITextField *nameTextField; //in either case, don't forget to synthesize the property - (IBAction)sayHello; @end

Next, we add the method textFieldShouldReturn: to the source file. ViewController.m


#import "ViewController.h"

Nicolaas tenBroek (AMDG)

80

iOS Development ViewController.m


@implementation ViewController @synthesize nameTextField; - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; [self sayHello]; } return YES;

Basic User Interface in iOS

- (IBAction)sayHello { UIAlertView *hello = [[UIAlertView alloc] initWithTitle:@"Hello!" message:[NSString stringWithFormat:@"Hello %@!", self.nameTextField.text] delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [hello show]; //if not using ARC: //[hello release], hello = nil; } #pragma mark - View lifecycle //some parts of the templated code have been removed for brevity they are not necessary here } - (void)viewDidUnload { [super viewDidUnload]; //use the line below if not using ARC //self.nameTextField = nil; } /* You will need this method if you are not using ARC - (void)dealloc { [super dealloc]; } */ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.nameTextField.text = @"John Doe"; } @end

You will notice three lines of code in our textFieldShouldReturn: method. First, the passed-in text field is instructed to resign its first responder status. That literally means the text field is no longer the focus

Nicolaas tenBroek (AMDG)

81

iOS Development

Basic User Interface in iOS

of the user input. When a text field is not the focus of user input, the keyboard automatically disappears. The next line of code calls our sayHello method. While this call was not required, it is helpful for the user as it saves them an additional button press. When responding to the return button on the keyboard you should always ask yourself if there is anything else that can be done for the user at that point. Saving the user an additional interaction will make your application much more pleasant to use, and is the behaviour expected by most people. The return value from this method indicates whether or not the text field should carry out its default behaviour in response to the return button press. For a text field the default action is to call an action method (just as with the button). We are not using the action method, so it really does not matter whether we return YES or NO. We could have used a different approach in our design and tied our sayHello action method to both the button and the text field. In that case we would not have called the method directly from textFieldShouldReturn: and then the return value of YES would be required to make things work properly. For now though, we will keep things a bit simpler. With the code updated, we need to go back into Interface Builder and wire up the delegate. This time the direction of the wire up will be reversed. Rather than right clicking on Files Owner and dragging to a component, we will right click on the component (specifically the text field) and drag to Files Owner. When we right click on the text field we get the following pop-up:

You will notice that this pop-up gives us access to the delegate, all the events (IBActions) the control can respond to, and even the referencing outlets (IBOutlets). In fact, for most connections it really does not matter whether you right-click on the Files Owner or on a component. You will be able to wire up a connection either way. The delegate is one example where the target of the right click does matter. The option for a delegate will not show up in the Files Owner dialogue until after the connection has been wired up, so right clicking on the text field was our only option.

Nicolaas tenBroek (AMDG)

82

iOS Development Now, simply drag from the delegate to Files Owner.

Basic User Interface in iOS

Save, build, and run, and your application will now respond to the return key from the keyboard and the keyboard will disappear when no longer needed. Now that we have seen how a basic application can be built and run, lets take a closer look at the components themselves. We will start with the UIView.

Nicolaas tenBroek (AMDG)

83

iOS Development

Basic User Interface in iOS

UIView
The UIView class is a fairly basic one, but at the same time incredibly important. UIView is actually the base class for all of the UI components, so everything it does all UI components can do. UIView extends UIResponder, which provides methods for handling touches, motions, and for handling the first responder status. A first responder is an element that is designated as the receiver of user input (for instance, from the keyboard). To those methods the UIView adds a number of properties that all components need. We will take a quick look at each of the properties, organised by function. Appearance There are three main properties that affect the appearance of the view: alpha, backgroundColor, and hidden. While the use of backgroundColor is perhaps obvious, the alpha property controls the opacity of the view. Changing this property can make any control more or less opaque. It ranges from 0.0 (clear) to 1.0 (opaque). If a component overlays a dark coloured screen, changing the alpha can also make the component appear to be more or less bright. The hidden is a BOOL that controls whether or not a view is displayed. One of the other appearance properties has a slightly confusing name: opaque. Opaque is a Boolean property that controls how the system treats a View while drawing it, but does not affect the actual opacity of the View. If opaque is set to YES, then the system can use optimised methodologies for drawing it, if set to NO, then additional calculations have to be made. If you change the alpha property of a View, you should also set the opaque value to ensure consistent drawing results. Other appearance properties include: autoresizesSubviews, autoresizingMask, clipsToBounds, contentMode, contentScaleFactor, and contentStretch. The auto re-size and content properties all control how the view responds to being placed in a different size or shaped container. For instance, the contentScaleFactor is a numeric factor that can be used to scale the display for different screen sizes. The clipsToBounds property indicates whether or not the view can attempt to display parts that lay outside the bounds of the views container. Size and Position There are several properties that control the size and position of the view itself. Those properties are: centre, bounds, frame, layer, and transform. While all of these properties can be set in code, right now we will use Interface Builder to control these items, as their interactions are quite complex. These properties receive a full treatment and exposition in the chapter on animations. Touches UIView and all its subclasses have the ability to respond to user touches. The properties involved are: exclusiveTouch, gestureRecognizers, multipleTouchEnabled, and userInteractionEnabled. Three of these, exclusiveTouch, multipleTouchEnabled, and userInteractionEnabled are simple BOOLs. User Interaction here is any user input including but not limited to touch. It can include keyboard and other inputs as well. The last of these properties, gestureRecognizers, is an array of objects that inform the view when a gesture is recognised. Gestures can be any sort of pre-defined sequence of touches, from taps (single, double, or more), swipes, shakes, or most anything else you might think of.

Nicolaas tenBroek (AMDG)

84

iOS Development

Basic User Interface in iOS

Containers All UIView objects can be containers of or contained by other UIView objects. This literally means that every screen you present to the user is made up of UIViews placed inside of other UIViews. There are three properties that help us manage the containment: subviews, superview, and window. The subviews property is an array of UIViews that are contained within the current view. The superview property is a reference to the view that contains the current view. If the current view is the top view or has not yet been added to a view, the superview property will be nil. One view can be added to another view through the method addSubview:, but oddly, to remove a view, you must call the method removeFromSuperview on the subview instance. The window is a reference to the window that contains the hierarchy of views displaying the current view. Window will be nil if the current view has not yet been added to another view. The vast majority of apps use only one Window object and display additional screens by asking the window to display different views. Tag The last property is tag. This is a simple integer that the programmer can set and then use in code to access a particular view. There will be a few instances where tag is useful as it offers a way to programmatically differentiate between controls (we will examine some examples later), but most of the time you can safely ignore this property. Now that we have a general idea of what a UI component looks like, lets take a look at the specific components we used in our example.

Nicolaas tenBroek (AMDG)

85

iOS Development

Basic User Interface in iOS

UILabel
UILabel is a component designed specifically to present a small amount of read-only text (if a large amount of text is needed, use the UITextView component). The text to be displayed can be hard coded or adjusted dynamically at run time. By default the label displays a single line, but you can increase that number if need be. The main data property of a label is call text, and holds the NSString to be displayed within the label. You have a great deal of control over the appearance of the text through the properties: font, textColor, textAlignment, highlightedTextColor, shadowColor, shadowOffset, and lineBreakMode. The purpose of most of these is perhaps obvious as they are very well named, but the line break mode bears further examination. The UILabel has the ability to dynamically adjust the display of its text when the number of characters is too large to fit the space provided using the font size indicated. For instance, one of the things the label can do is to dynamically reduce the font size to make the words fit. You can control how far down the size can be adjusted through the minimumFontSize property. You can also force the label to use the indicated font size by setting the property adjustsFontSizeToFitWidth to NO. Once the label has determined that the font size cannot be further reduced, it will switch strategies and attempt to find another way to fit the text. This is when the lineBreakMode property comes into play. If the number of lines is more than one you could chose to have the text word wrap, or character wrap. Word wrapping attempts to break the string at word boundaries, character wrapping ignores those boundaries and may wrap in the middle of a word. Obviously, if your label has only a single line, wrapping is not a viable option. In that situation you can either set the break mode to clip or truncate. The clipping process simply renders as much of the text as it can until the edge of the drawable area is encountered. Once the edge has been reached the label quits drawing, perhaps even with a partially displayed character. Truncation takes a slightly different approach. Instead of running head-on into the wall of the component it displays as much of the text as possible while reserving space for an ellipsis. There are three modes of truncating the displayed text: head, tail, or middle, and depending on your choice of truncation the ellipsis will be displayed at the beginning, end, or middle of the text. Truncated strings will never display a partial character like clipping and have the advantage of notifying the user that more text is present (though it does not provide a mechanism which allows the user to see the entire text). This often makes truncation a more attractive option than clipping, but clipping can actually display more characters than truncation because it does not need the ellipsis.

Nicolaas tenBroek (AMDG)

86

iOS Development

Basic User Interface in iOS

UITextField
The UITextField is designed for allowing the user to enter or edit a single line of text (again, for multiple lines use the UITextView control). It has many of the same display properties as the UILabel, but instead of truncating text, it simply scrolls it out of the controls visible surface. Oddly, like the label it can reduce the font size as the amount of text increases, and uses the same properties to control the minimum font size. Scrolling only begins after the minimum font size has been reached. The text field also adds some more interesting properties like the placeholder and clearButtonMode. The placeholder property is a string that will be displayed if the text property is either nil or an empty string and serves as a prompt for the user, informing them of the nature of the required entry. The placeholder text is displayed in a smaller and lighter font than the clear black default of the text field, so that the user does not think something has already been entered in that location. When the text field becomes the first responder or the text property is set in code, the placeholder text automatically disappears. It will reappear any time the text fields text is entirely removed. The clear button is a small circle with an x in it that can be displayed within the text field and deletes all the text in the textfield when pressed. This button allows the user to quickly remove the text in a much more convenient fashion than backspacing over each character. Using the clearButtonMode we can adjust when the button is displayed. The display choices are never display, always display or display only while the field is being edited. If you choose to have it always displayed, be sure to account for the space occupied by the button by making the text field a bit wider in your layout. As an alternative to the clear button the text field can be set to clear automatically when the text field becomes the first responder with clearsOnBeginEditing. To avoid frustrating your users, you should only set this property in special circumstances; as having text disappear when you are simply trying to correct a mistyped entry is especially irritating. Entering a password would be a good example of an acceptable time to erase the text when the user selects a field. Entering a username, URL, or most anything else would be examples of times when automatically clearing is simply unacceptable. The UITextField also implements the UITextInputTraits protocol, which allows for some interesting behaviours. For instance, if the data in the text field should not be displayed on the screen (i.e. the user is typing in a passkey), then you can enable the secureTextEntry property. Setting this property forces the text field to display each character for about one second or until another character has been typed. Once either of those two events happens the character is replaced on the screen with a large dot. The UITextInputTraits also include auto correction and auto capitalisation. These options are terribly useful if the user is typing in common words, but absolutely disastrous if the user is typing in names, passwords, URLs, or anything else. Think carefully before enabling these options. If the input is anything other than straight text, leave these options disabled. The UITextInputTraits protocol also gives us the ability to change the keyboard that is displayed by default for a particular text field using the keyboardType property. The keyboards supported are: standard alphabetical, numbers and punctuation, URL, number pad (literally just the digits 0 through 9 on large buttons), phone pad (the same as the number pad with two additional buttons for * and #), Nicolaas tenBroek (AMDG) 87

iOS Development

Basic User Interface in iOS

email address, and decimal pad (which is the number pad with an additional button for the decimal character). Like most other properties, selecting the keyboard to display is simply a matter of choosing it from a drop-down list in the Attributes Inspector. Taking a moment to select the proper keyboard for each text input field would go a long way toward making your app friendlier for the user. Keep in mind that users really dont enjoy the process of searching for the proper key; they would rather have it displayed for them right away. While you are selecting the proper keyboard for display, also consider the return button. By default the return key is always enabled, but you can set that to automatic. Setting the key to be enabled automatically disables the key when the text field is empty and enables it the moment the user has entered some text. This is a useful idiom to indicate to the user that the text is required. You can also customise the text that is displayed on that button as well. There are a large number of strings to choose from, and you should select one that is appropriate for the action the user will be performing by entering the text. The available strings are: return, go, Google, Join, Next, Route, Search, Send, Yahoo, Done, and Emergency Call. Keep in mind that the action taken by the button should be reflected by the text displayed. Choosing anything else will frustrate your users.

Nicolaas tenBroek (AMDG)

88

iOS Development

Basic User Interface in iOS

UIButton
The UIButton is an extremely simple and flexible control for allowing the user to fire off an action. The button is actually made up of three different controls: a background image view, a foreground image view, and a label. You can even add different images to the button to be displayed as it changes state between disabled, normal, highlighted, and selected. The button control has a property called buttonType which controls what kind of button will be displayed. The custom type allows you to control the display through the image and label combinations. The rounded rectangle type is the default iOS button for displaying text (and is the kind we used in our example). Other types include a detail disclosure that is used with tables to indicate more data is available, two info buttons (light and dark), and an add button that displays a large plus sign. There are obviously many more controls available in the iOS library, and the platform supports the creation of custom controls, giving us nearly unlimited options. All of the controls share certain features, and that makes learning to use them quite a bit easier. For instance, all controls extend UIView, so we know the common starting ground for the controls already. Additionally, the appearance and behaviour of the controls are usually configurable through properties. Most (though definitely not all) properties can be configured at design time through Interface Builder, and all properties can be configured at run time in your code. Data is most often stored in properties, and many controls support the use of IBAction methods to notify observers when events happen. Given this framework, learning to use a new control is a fairly simple matter that will take you at most a few minutes with the documentation.

Nicolaas tenBroek (AMDG)

89

iOS Development

Basic User Interface in iOS

UI Control Overview
While we certainly do not intend to attempt to explore every UI control in detail, an overview of the most common controls would be valuable. Control Name UIActionSheet UIActivityIndicatorView UIAlertView UIBarButtonItem UIButton UIDatePicker UIImagePicker UIImageView UILabel UIMenuController UIMenuItem UINavigationBar Description Presents a title and a set of buttons. Typically used to present users with a list of alternative actions or to warn users of potentially dangerous or permanent action. Offers a spinning graphic to inform users that a task of indeterminate length is being processed. Displays a title, message, and buttons in an alert dialogue. Typically used to inform the user that an action has occurred and may offer a chance to respond. A button specifically designed for use on UIToolbars and UINavigationBars. A basic button. Can display a label, a graphic, or both. Different button states (Disabled, Highlighted, Normal, and Selected) can each have a custom appearance. A picker that displays multiple rotating cylinders that allow users to select a date, a time, or both. Provides an interface that allows users to take new pictures and movies or select from those stored in the photo libraries. Displays an image. Can be a single image or an animated set of images through a slideshow. Displays a small amount of read-only text. While not technically a UI component, this Controller displays a context menu on a screen. Often used to display the Cut | Copy | Paste editing menu. A button that can be displayed on a UIMenuController. A bar typically displayed at the top of the screen which assists with navigating hierarchical content. Divided into three sections, the bar typically displays buttons and a title. This control is discussed in detail in the chapter on navigation. Used to create and manage a set of pages. Each page is represented as a dot on the control with the dot representing the currently displayed page highlighted. User interaction causes the display to be advanced one page in the direction indicated by the tap. A customisable set of spinning cylinders that allow the user to select items from a set. The number of cylinders is configurable. Not technically a UI component, this controller displays a view layered on top of another view without obscuring the entire screen. The popover view is not modal, so the user can still access the underlying view. Used to indicate progress through a task with a determined length. Provides support for displaying other UI components on a view that can larger than the devices screen. Scrolling behaviour is automatically handled by the component in response to user gestures. Scrolling can also be initiated programmatically.

UIPageControl

UIPickerView UIPopoverController UIProgressView UIScrollView

Nicolaas tenBroek (AMDG)

90

iOS Development Control Name UISearchBar

Basic User Interface in iOS Description A complex control for facilitating searching. Offers a customised text field, bookmark and cancel buttons. The text field has rounded corners and displays a magnifying glass icon. Note: This component does not implement a search, so you must do that part. A horizontal bar made up of multiple discrete button-like items. The buttons are in immediate proximity to one another, separated by a line. The buttons can act as regular (momentary) or latching buttons. A bar with a thumb for selecting from a range of values. While the bar does not display the actual values, it does support minimum and maximum icons that can be used to inform the user of the kinds of values they are selecting. A segmented-style control with two buttons: one labelled + and the other labelled -. The stepped is designed to allow incrementing and decrementing of numeric values and is new in iOS 5. Yet another item which is not technically a UI component. This iPad only controller displays two different views side by side. This control is discussed in detail in the chapter on navigation. Displays a switch for selecting between two values. In addition to graphical position and colour, the switch indicates its state by displaying either ON or OFF. Displays two or more buttons and typically used to select the view to be displayed. Buttons are always latching buttons. Allows for user configuration by reordering the buttons if desired. This control is discussed in detail in the chapter on navigation. A button-like item, which can be displayed on a UITabBar. Supports text and an image as well as an optional badge. The badge is a red oval containing text and displayed on the top of the button. Displays a table. This control is discussed in detail in the chapter on tables. A customisable cell that can be displayed in a UITableView. This control is discussed in detail in the chapter on tables and cells. A one-line text entry area designed for input of small amounts of text. A multi-line text area designed for entry or display of large amounts of text. Can be read-only for display purposes. This component is capable of recognising phone numbers and addresses and responding appropriately to selection of those items. One word of caution: If your text view is editable, you must explicitly disable the data detection. Users will be terribly unhappy if each time they try to select a phone number in order to edit it, the phone number is dialled instead. A control that displays multiple buttons in the form of UIToolbarItems. Buttons are always momentary in behaviour. The base class for all UI components. Can also be used as the base for a screen. The UIView is capable of containing and displaying other UIView components. A UIView that uses Safari to display web content. You can use the UIWebView to display HTML documents within your application, or to provide access to the web from within your application. This control also supports JavaScript injection into the page being displayed.

UISegmentedControl

UISlider

UIStepper

UISplitViewController

UISwitch

UITabBar

UITabBarItem UITableView UITableViewCell UITextField

UITextView

UIToolbar UIView

UIWebView

Nicolaas tenBroek (AMDG)

91

iOS Development Control Name UIWindow

Basic User Interface in iOS Description This component is designed to be the root of a UIView hierarchy. It provides support for displaying a UIView on the screen and implements a coordinate system for the UIView to use in its layout. Typical iOS applications have one UIWindow.

As you can see, there are a fair number of built-in controls. As we mentioned before, they all designed to work within an MVC architecture and share common design principles. With these controls you can design an incredible array of apps

Nicolaas tenBroek (AMDG)

92

iOS Development

Basic User Interface in iOS

Nicolaas tenBroek (AMDG)

93

iOS Development

Protocols, Delegates, and Categories

Protocols, Delegates, and Categories


We briefly saw how to use protocols and delegates in the previous chapter, but they are worth investigating in detail to more completely understand their purpose and use. Data type abstraction is a key component in Object Oriented Programming. The more abstract and generic you can make your code, the easier it will be to extend or modify. Ultimately abstract code results in far less work for programmers as abstract code is much more reusable than specific code.

Delegates
In real life a delegate is a person who is authorised to act on behalf of another person. In programming a delegate is an object designated as the receiver of method calls, so the definitions are actually quite similar. Delegates work by implementing a known set of methods and then identifying themselves to objects that generate calls to those methods. Because a delegate must implement a known set of methods, they are often used with protocols, though protocol use is not required. By convention, when a delegate is required we specifically name the property or method argument delegate to reduce confusion. This is not a keyword, but rather a programming convention used for its clarity of communication. When you see a property or an argument named delegate you know that you can register an object to be called when events happen. You also know that it will be worth your time to read the documentation to discover what methods will be called.

Protocols
A protocol is a methodology that allows us to indicate a set of method signatures a given class will implement without actually involving inheritance. We say that a class implements a protocol to differentiate it from extending a class as you would in an inheritance situation. A protocol is basically an abstraction of a class that is best used when you know the functionality required for a situation, but do not know how that functionality will be implemented. The abstraction then takes the form of a collection of method signatures. The actual implementation of those methods is left for a later time, but we can write the code that uses those signatures as if they really exist (i.e. you can call the as yet nonexistent methods). To see how a protocol can be useful, we will first consider a situation where we do not have access to a protocol. For this example lets assume we have a fictional UI control, and the class below provides the code for the control. FictionalControl.h
#import <UIKit/UIKit.h> @interface FictionalControl : UIControl { } - (void)userInputStarted:(UIEvent *)event; - (void)userInputEnded:(UIEvent *)event; @end

Nicolaas tenBroek (AMDG)

94

iOS Development FictionalControl.m


#import "FictionalControl.h" @implementation FictionalControl - (void)userInputStarted:(UIEvent *)event { //some generic event handling code here } - (void)userInputEnded:(UIEvent *)event { //some generic event handling code here } @end

Protocols, Delegates, and Categories

If someone wants to use an instance of FictionalControl and be notified of the user input events they would be forced to subclass FictionalControl, and then override the methods userInputStarted: and userInputEneded:. Hopefully, they would also remember to pass those method calls up to the super class methods so we do not lose the event handling code we already put into place. The situation continues to worsen as the programmer will likely be forced to write a subclass for each instance of FictionalControl they want to use. Then, the programmer would either need to inform the Controller class of each instances type, or create the instances in the Controller itself. Each of these subclass requirements increases the amount of time it takes to develop the app, increases our chance for bugs, and decreases the flexibility of the code by making it more and more specific. Our fictional Controllers interface might look like this: FictionalAppViewController.h
#import <UIKit/UIKit.h> #import "FictionalControlSubclass1.h" #import "FictionalControlSubclass2.h" #import "FictionalControlSubclass3.h" //more imports here, one per subclass @interface FictionalAppViewController : UIViewController { FictionalControlSubclass1 *fc1; FictionalControlSubclass2 *fc2; FictionalControlSubclass3 *fc3; //more variables here, at least one per subclass } @end

Now, lets reconsider the situation assuming we have a protocol in place. We wont worry about the details of creating a protocol yet, we will simply assume we have a protocol called

Nicolaas tenBroek (AMDG)

95

iOS Development

Protocols, Delegates, and Categories

FictionalControlDelegate, and that it contains the methods userInputStarted: and userInputEnded:. Our Controller code changes to a much simpler version. FictionalAppViewController.h
#import <UIKit/UIKit.h> #import "FictionalControl.h" @interface FictionalAppViewController: UIViewController <FictionalControlDelegate> { } @end

Notice we added the protocol name in a set of angle brackets at the end of the interface signature. Multiple protocols can be implemented by separating the protocol names with commas. Next, in our Controllers .m file, we need only implement the two methods: FictionalAppViewController.m
#import "FictionalAppViewController.h" @implementation FictionalAppViewController - (void)userInputStarted:(FictionalControl *)control withEvent:(UIEvent *)event { //event handling code here } - (void)userInputEnded:(FictionalControl *)control withEvent:(UIEvent *)event { //event handling code here } @end

Then wire up the controls in Interface Builder and we are all set. This is a much cleaner and simpler situation, which means more flexibility and fewer bugs. Now lets take a look at how we can create our own protocols by actually creating our FictionalControlDelegate protocol. To do that we will need to return to our fictional controls source code.

Nicolaas tenBroek (AMDG)

96

iOS Development FictionalControl.h


#import <UIKit/UIKit.h> @protocol FictionalControlDelegate; @interface FictionalControl : UIControl { id<FictionalControlDelegate> delegate; }

Protocols, Delegates, and Categories

@property (nonatomic, strong) id<FictionalControlDelegate> delegate; - (void)userInputStarted:(UIEvent *)event; - (void)userInputEnded:(UIEvent *)event; @end @protocol FictionalControlDelegate - (void)userInputStarted:(FictionalControl *)control withEvent:(UIEvent *)event; - (void)userInputEnded:(FictionalControl *)control withEvent:(UIEvent *)event; @end

There is a lot going on here so lets take it a bit at a time. Starting at the very top of the file we see a protocol prototype. This is necessary because of the one-pass nature of the Objective-C compiler. We are unable to use the protocol as a type until it has been declared, and we did not declare it until after the interface was declared. Of course, we could move the entire declaration of the protocol to the top of the file, but then we run into another problem, because the protocol references the type declared by the interface. So we have a chicken-and-egg type problem. The protocol prototype works exactly like a method prototype to solve that problem by telling the compiler we have a protocol named FictionalControlDelegate and it will be declared later. Next lets look at the instance variable we created called delegate. You will notice the data type is listed as id<FictionalControlDelegate>. That literally translates to any object that implements the protocol FictionalControlDelegate. We are completely uninterested in the actual type of object we have as a delegate. We need only to know that it implements our delegate because the delegate methods are the only methods we are planning to call. If for some reason we needed a specific object type we could replace id with that type, but doing so would make our code less generic. Change the type to something more specific only when you absolutely need something more specific (i.e. you are planning to call methods from that specific class). Down a little further in the file we see the protocol itself. It begins with the directive @protocol followed by the name of the protocol. Then we simply list the method signatures that form the protocol

Nicolaas tenBroek (AMDG)

97

iOS Development

Protocols, Delegates, and Categories

and end it with the @end directive. Creating a protocol is as simple as that. It is nothing more than a collection of method signatures. With our new protocol defined, and a delegate property in place, we can now call the methods from the protocol in our .m file. FictionalControl.m
#import "FictionalControl.h" @implementation FictionalControl @synthesize delegate; - (void)userInputStarted:(UIEvent *)event { //some generic event handling code here [delegate userInputStarted:self withEvent:event]; } - (void)userInputEnded:(UIEvent *)event { //some generic event handling code here [delegate userInputEnded:self withEvent:event]; } @end

Here we see the calls to the methods from our protocol. Note that we have no idea here how those methods have been implemented, nor do we know what kind of class has implemented them. We do not need that information though. We need only to know that the methods are available, and thus we can call them. A protocol does not have to be declared in the same file as a class that is using it. You certainly may create a header file just for the protocol, or even declare multiple protocols in the same header file. How you structure the code is up to you, but some simple guidelines will make the job a bit easier. If the protocol is going to be used by one class only (as in our example), declare it in the header file of that class. If the protocol is general enough to be of use to multiple classes, or is part of a family of protocols, then write a separate header file. Simply remember to include the header file when using the protocol. Optional Methods Objective-C takes an interesting twist with protocols and adds support for optional methods. These are methods that may or may not be important to the receiver object, so that class can implement them if they are needed. By default all methods in a protocol are required meaning that you can assume they are implemented. The compiler will issue warnings for any class that fails to implement required

Nicolaas tenBroek (AMDG)

98

iOS Development

Protocols, Delegates, and Categories

methods of a protocol. A class that claims to implement the protocol and does not provide the required methods will cause app crashes when used (and that is actually the desired outcome). Optional methods literally mean that the implementing class may or may not provide the method, and their absence must never cause an app to crash. Adding an optional method to a protocol is a fairly simple affair, but when calling the method the situation becomes more complicated. One cannot simply call a method marked as optional and assume it will work as we do with the required methods. Optional methods must never cause an app to crash due to their absence. So, before we can call an optional method we must check to see whether or not the implementing class has actually implemented the method. As a demonstration, we shall add an optional method to our FictionalControlDelegate protocol. FictionalControl.h
#import <UIKit/UIKit.h> @protocol FictionalControlDelegate; @interface FictionalControl : UIControl { id<FictionalControlDelegate> delegate; } @property (nonatomic, strong) id<FictionalControlDelegate> delegate; - (void)userInputStarted:(UIEvent *)event; - (void)userInputEnded:(UIEvent *)event; @end @protocol FictionalControlDelegate - (void)userInputStarted:(FictionalControl *)control withEvent:(UIEvent *)event; - (void)userInputEnded:(FictonalControl *)control withEvent:(UIEvent *)event; @optional - (void)someOptionalMethod:(FictionalControl *)control; @end

In this updated version of our protocol we have introduced the @optional keyword. There is a matching @required keyword, but as we mentioned earlier, this is the default setting, so it is not often used. The keywords work in the same fashion as the access specifiers do for instance and class variables. Every

Nicolaas tenBroek (AMDG)

99

iOS Development

Protocols, Delegates, and Categories

method listed after @optional is optional until the end of the protocol definition is reached or until the @required keyword is used. Now that we have defined an optional method, we can attempt to call it in our .m file. FictionalControl.m
#import "FictionalControl.h" @implementation FictionalControl @synthesize delegate; - (void)userInputStarted:(UIEvent *)event { //some generic event handling code here [delegate userInputStarted:self withEvent:event]; } - (void)userInputEnded:(UIEvent *)event { //some generic event handling code here [delegate userInputEnded:self withEvent:event]; if([delegate respondsToSelector:@selector(someOptionalMethod:)]) { [delegate someOptionalMethod:self]; } } @end

Notice that we called the method respondsToSelector: as a check before calling the optional method from the protocol. This check ensures that the delegate will indeed have a method with that signature and the call to the method will succeed. The method respondsToSelector: is inherited from NSObject. NSObject also defines a class version of the method (rather confusingly) called instanceRespondsToSelector: that allows you to check the class for the existence of a method rather than testing a specific instance of the class. It cannot be overstated that this setup is absolutely required when using optional methods. Failure to employ the respondsToSelector: check effectively turns methods marked as @optional into required methods.

Nicolaas tenBroek (AMDG)

100

iOS Development

Protocols, Delegates, and Categories

Protocol Inheritance If a protocol exists that has nearly everything you need, you can actually extend that protocol and add your own methods to it. The code for that is remarkably easy:
..... @protocol MyProtocol <SomeOtherProtocol> //new methods here @end ..

As with classes implementing protocols you can extend or combine multiple protocols by separating their names with commas. This approach can be useful if you have a number of classes which all need to implement the same set of protocols. The NSObject protocol provides access to the release and retain methods for memory handling, so it is often a good protocol to extend. Consider it each time you create a new protocol as objects that implement protocols often need to be stored for long periods.

Nicolaas tenBroek (AMDG)

101

iOS Development

Protocols, Delegates, and Categories

Categories
Objective-C has an interesting feature called Categories. A Category is basically a methodology for injecting code into an existing class that is a completely separate process from creating a subclass. Consider a situation where you have a class A, and then create a subclass called B. The existence of the new class B has no impact whatsoever on the instances of class A that you create and use. You can create instances of both A and B side by side with no trouble at all. Additionally, instances of class B cannot access any private instance data from class A, so the encapsulation and implementation hiding are intact. A Category is an entirely different process. With a Category you write new methods or override methods just as you do with sub-classing, but you do not actually create a new class. Those methods are injected into instances of the original class and can even access private instance data from that original class. This is true even if you do not have access to the code from the original class. Furthermore, the code can be injected into all instances of the original class, regardless of where they are used within your app. Creating a Category is as simple as writing an interface using the same name as the original class, then adding the Category name in a set of parentheses after the class name, and the new method prototypes (Categories cannot add new instance variables). Finally, write the method code in an .m file and the category is complete. To demonstrate a Category in action lets develop a small Single View application called Categories (use ARC, but do not use the Storyboard). We will create a view with text fields allowing us to enter a customers first name, last name, and ID. We will also add a button to create a Customer object and a label to display the Customers description. Here is our simple screen:

Nicolaas tenBroek (AMDG)

102

iOS Development

Protocols, Delegates, and Categories

Next, we will need to write some code beginning with our Customer class. We will need a new set of files for this class, which we can add quite easily. First, right-click on the Categories folder (or the one with your project name if you used a different name), and then choose New File:

Next, select the Cocoa Touch library in the iOS section and choose Objective-C class from the options:

Name the class Customer, and select NSObject as the subclass:

Nicolaas tenBroek (AMDG)

103

iOS Development

Protocols, Delegates, and Categories

Finally, press the Create button to create the file. With the files created we can edit them as follows: Customer.h
#import <Foundation/Foundation.h> @interface Customer : NSObject { @private NSString *firstName; NSString *lastName; long customerID; } - (id)initWithFirstName:(NSString *)newFirstName lastName:(NSString *)newLastName customerID:(long)newID; @end

Customer.m
#import "Customer.h" @implementation Customer - (id)initWithFirstName:(NSString *)newFirstName lastName:(NSString *)newLastName customerID:(long)newID { if(self = [super init]) { firstName = newFirstName; lastName = newLastName; customerID = newID; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %@", firstName, lastName]; } @end

Notice that the variables are all private, so from the sub-classing perspective, they are safe from outside modification. We did not prototype the description method because it is inherited from NSObject, and we are simply overriding it. Now that the Customer class is done, lets move on to our controller.

Nicolaas tenBroek (AMDG)

104

iOS Development ViewController.h


#import <UIKit/UIKit.h>

Protocols, Delegates, and Categories

@interface ViewController : UIViewController <UITextFieldDelegate> @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet UITextField *firstNameField; UITextField *lastNameField; UITextField *idField; UILabel *descriptionLabel;

- (IBAction)createCustomer; @end

ViewController.m
#import "ViewController.h" #import "Customer.h" @implementation ViewController @synthesize @synthesize @synthesize @synthesize firstNameField; lastNameField; idField; descriptionLabel;

- (IBAction)createCustomer { Customer *customer = [[Customer alloc] initWithFirstName:firstNameField.text lastName:lastNameField.text customerID:[idField.text intValue]]; descriptionLabel.text = [customer description]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } @end

Dont forget to go wire-up all the properties, the IBAction, and the text field delegates! Once all that has been done we can run our app. After entering some data and pressing the Create Customer button we see our description appear as expected.

Nicolaas tenBroek (AMDG)

105

iOS Development

Protocols, Delegates, and Categories

So far, everything is pretty normal, with nothing remarkable happening. Now lets create a Category. Add another new class, this time named CustomerCategory. CustomerCategory.h
#import <Foundation/Foundation.h> #import "Customer.h" @interface Customer (CustomerCategory) - (void)upCaseNames; @end

Our interface is named Customer, just as the original class was named. Note that we imported the Customer class, so we are effectively redefining it. The category name then appears in parentheses after the interface name. As we are not allowed to add new variables, there are no French braces after the interface names. Instead we simply add the new method prototypes. Now we can write our .m file. CustomerCategory.m
#import "CustomerCategory.h" @implementation Customer (CustomerCategory) - (void)upCaseNames {

Nicolaas tenBroek (AMDG)

106

iOS Development CustomerCategory.m


firstName = [firstName uppercaseString]; lastName = [lastName uppercaseString]; }

Protocols, Delegates, and Categories

- (NSString *)description { return [NSString stringWithFormat:@"%@, %@ (%ld)", lastName, firstName, customerID]; } @end

Notice that in the new method upCaseNames we directly modify the private instance variables. This is completely legal according to Objective-C. We also overrode the description method to use a different format and include the customer number. Just as with inheritance, prototypes are not required for any method being overridden. Rerunning our app we see the effects of merely adding this code to our project. Notice, we did not change our controllers code at all, and yet the new description method was called!

Now, lets modify our controller so that it has access to the new method we added. To gain access to the new method we will need to change our #import statement from importing Customer to importing CustomerCategory. When using a category you do not need to specify the import of the original class because that import is already contained in the categorys header file.

Nicolaas tenBroek (AMDG)

107

iOS Development

Protocols, Delegates, and Categories

After modifying the import statement we can add a call to the new upCaseNames method. ViewController.m
#import "ViewController.h" #import "CustomerCategory.h" @implementation ViewController @synthesize @synthesize @synthesize @synthesize firstNameField; lastNameField; idField; descriptionLabel;

- (IBAction)createCustomer { Customer *customer = [[Customer alloc] initWithFirstName:firstNameField.text lastName:lastNameField.text customerID:[idField.text intValue]]; [customer upCaseNames]; descriptionLabel.text = [customer description]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } @end

And the result is clear to see. Notice that we did not change anything about how we created the object, we were able to continue using the Customer as before, but the class appears to contain a new method.

Nicolaas tenBroek (AMDG)

108

iOS Development

Protocols, Delegates, and Categories

At this point you are likely worried about the immense security risk posed by Categories. It is remarkably easy to inject code into an existing class, and even modify variables marked as private, completely changing the behaviour of the class. While you cannot completely protect yourself from the danger posed by this quirky feature, there is an additional step you can take to protect your variables, if not your methods. Any variable you wish to protect (which should be each and every instance level variable) can be declared in the .m file rather than the .h file. If we move the variable declarations from Customer.h to Customer.m the data would be much more protected. Those files should now look like this: Customer.h
#import <Foundation/Foundation.h> @interface Customer : NSObject - (id)initWithFirstName:(NSString *)newFirstName lastName:(NSString *)newLastName customerID:(long)newID; @end

Nicolaas tenBroek (AMDG)

109

iOS Development Customer.m


#import "Customer.h" @implementation Customer { @private NSString *firstName; NSString *lastName; long customerID; }

Protocols, Delegates, and Categories

- (id)initWithFirstName:(NSString *)newFirstName lastName:(NSString *)newLastName customerID:(long)newID { if(self = [super init]) { firstName = newFirstName; lastName = newLastName; customerID = newID; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %@", firstName, lastName]; } @end

After making those quite minor changes your CustomerCategory will now generate compile errors, which is exactly what we wanted to happen. As an added bonus, declaring the variables in the .m file ensures that more of our implementation is hidden which is good OOP practice. Unfortunately, any methods we want to keep private are still exposed in the header file, so keeping the variables safe is only a small win. Oddly enough, Categories can rescue us from this situation. Objective-C does not have a mechanism for creating private methods, which means any method prototype in the header file can be called by any other class. You can however use a Category declared in your .m file to effectively create private methods. Because the category is declared in the .m file, no other class can see it (remember that other classes only import the header file and therefore can only see what is in that file). This is a terribly useful workaround for the lack of private methods, so we will demonstrate its use. For our example we will add a private method to our Customer.m that ensures we do not have nil strings in the names. No change is needed to the .h file, and the changes in the .m file are presented in bold.

Nicolaas tenBroek (AMDG)

110

iOS Development Customer.m


#import "Customer.h" @interface Customer (CustomerPrivate) - (void)validateNames; @end @implementation Customer { @private NSString *firstName; NSString *lastName; long customerID; }

Protocols, Delegates, and Categories

- (id)initWithFirstName:(NSString *)newFirstName lastName:(NSString *)newLastName customerID:(long)newID { if(self = [super init]) { firstName = newFirstName; lastName = newLastName; customerID = newID; } return self; } - (NSString *)description { [self validateNames]; return [NSString stringWithFormat:@"%@ %@", firstName, lastName]; } - (void)validateNames { if((!firstName) || ([firstName length] <= 0)) { firstName = @"UNKNOWN_FIRST_NAME"; } if((!lastName) || ([lastName length] <= 0)) { lastName = @"UNKNOWN_LAST_NAME"; } } @end

The private method validateNames is now available within the class, and as expected, unavailable outside of the class. Protocols, delegates, and categories are all incredibly important parts of every iOS app. They will be used in virtually every class you write, so you will become comfortable with them quite quickly.

Nicolaas tenBroek (AMDG)

111

iOS Development

Protocols, Delegates, and Categories

Nicolaas tenBroek (AMDG)

112

iOS Development

Tables

Tables
iOS table UI control may well be the most useful of all the pre-built data presentation mechanisms in the library. The table has built-in support for labelling, scrolling, selecting, deleting, and even rearranging the data in the table. The default data presentation layouts are quite good, and adding support for your own layout is easy as well. The table is highly configurable, allowing you to use only the features you want by simply declaring support for them. Most of the features are disabled by default and can be made available in very simple ways. Together all of this creates a control that is both simple to use and powerful. You can implement a functional simple version of the table with very little effort and yet produce a good-looking display. Creating a customised display or enabling advanced functionality will take a little more work, but due to the codes authors diligence in following Object Oriented Design principles, even that work is not onerous. Like all controls the table is implemented using the MVC architecture. As the table displays its data elements in a list, the Model most often used with the table is an array, but you are free to use whatever mechanism you need. The tables Controller simply asks your Model for an element to display using a two-dimensional index. Obviously your Model needs to ensure that the same data is always returned for the same index because inconsistent data display will cause the users to become confused when navigating their way around the table, and may well cause your app to crash . Interestingly, the table control only displays data in one dimension. Specifically, the data is displayed in one column, with each unit of data in its own row. In that fashion it is more like a list than a true two dimensional table (like one would see in a spread sheet for example). If your data has more than one dimension the extra dimensions are displayed as sections within the table. Each section can be visually separated from the others and may optionally display a both a header and a footer. You will probably be surprised when you realise just how often the table is used in the iOS platform and how often it will be useful for you. For instance, virtually all of the settings are displayed using tables, as are the photo albums (if you have multiple albums), notes, contacts, email, calendar, and even all the data about the phone app, with the exception of the dial screen and the in-call screen. Numerous 3rd party apps have used it as well, which means users are quite familiar with navigating their way around the table and will instantly understand how to use your app if you employ it as well. Lets look at some table examples from the built-in iOS apps. This first screen shot below is from the iPhone settings app and is a wonderful example of the flexibility of the table. You can see the table divided into sections with related settings grouped together. While the sections themselves are not labelled on this screen, the table does support such labelling. Cells have icons displayed on the left, followed by a title, and some then add additional information in the form of controls (like the On/Off Slider) or labels. Almost all of these are also displaying the Disclosure Indicator (thats the right facing chevron) that tells the user more information will be displayed if the cell is selected.

Nicolaas tenBroek (AMDG)

113

iOS Development

Tables

Our second example is also from the settings app. In this screen shot you can see a section containing a label (Nicolaas tenBroek) and a disclosure indicator as well as cells that display information, but do not take any action. Note the lack of disclosure indicator in the second grouping. That informs us that this screen is the end of the line from a navigation standpoint.

The next example is from the contacts app and shows a different style applied to the separate sections. Rather than rounded corners all the cells are rectangular and extend to the edge of the screen. Labels are applied to the different sections and an additional navigation (the list of letters) is displayed on the right side of the screen. Tapping on a letter in the right side navigation will automatically scroll the table to display the section with that title.

Nicolaas tenBroek (AMDG)

114

iOS Development

Tables

Next, we will look at an example from the mail application. There are no separate sections in this table, though you can tell from the disclosure indicators that more screens await you by selecting a cell. You can also tell that the trash has 15 unread emails in it (we have found that it is a pretty efficient place to store all of those unread emails). Additionally, you can see that some folders are actually subfolders as they are somewhat indented to the right.

Our last example is from the iPod application. This view shows a very simple layout with a title in bold text and a subtitle in grey below. This example is also useful because it demonstrates what can happen when the text is too long to fit the screen. You probably already guessed that UILabels were used for the text display, and you can see the UILabels tail truncation setting in action here.

Nicolaas tenBroek (AMDG)

115

iOS Development

Tables

Nicolaas tenBroek (AMDG)

116

iOS Development

Tables

Creating a Basic Table


Now that you have seen several examples of the table in action, lets explore the classes and protocols that make the table work. The table is heavily modularised, with three classes and two protocols all working in conjunction. We will begin our examination with the class UITableView, as it is the largest and perhaps most important one. The UITableView class makes everything work together by controlling the display of elements on the screen and handles all user interaction with the table. This class contains properties for determining the look of the table including items like the background, the cell position layout, and cell height. It does not however govern the layout or display of the data within the cell. The UITableViews methods provide for the editing capabilities like insert, delete, and rearranging of the cells, scrolling (so you can programmatically scroll to a section), and access to individual cells or sections. The next class involved is the UITableViewCell. This class is responsible for controlling the display and behaviour of individual cells within the table. UITableViewCell has built-in support for four different layouts, the first of which is the default. This default layout is very simple and contains only an optional image on the left side of the cell followed immediately by a black, left-aligned label. The second layout contains a black, left-aligned label on the left side of the cell and a blue right-aligned label using a smaller font on the right side of the cell. Examples of this layout can be seen in the screenshot of the Info page of the Settings app that we examined earlier. The third layout uses a blue right-aligned label on the left side of the cell and a black left-aligned label on the right side of the cell. Examples of this layout can be seen in the detail screen of the Contacts app. The final layout uses a black left-aligned label across the top of the cell and a smaller grey left-aligned label across the bottom of the cell. Examples of this can be seen in the iPod application (and in the screen shot above). The layouts are all affected by the indentation level property. Indentation levels can be set to indicate that the displayed item is a sub-portion of the previous item. We saw an example of indentation in the earlier screen shot of the mail application. In that screen shot a folder named New Folder Here is displayed indented and below the trash folder. The indentation tells us that this folder is a sub-folder of the trash. The indentation level is represented by an integer and tells the cell to move everything to the right by a multiple of the indentation width. The indentation width is simply the number of pixels to shift. By default the indentation level is 10 pixels, but that can easily be modified in code. Each indentation reduces the space remaining for the display of the labels and can cause truncation of the display. Instances of UITableViewCell also support an accessory. The options for accessory are a disclosure indicator (the right facing chevron we saw in the screenshots earlier), a disclosure button (a round blue button displaying a white right facing chevron), and a check-mark. The accessories are used to indicate actions available to the user. The check-mark should be used in a single-select scenario to indicate that a cell has been selected from a list of possibilities. Cells have both a selected and a highlighted property built-in, so management of the selecting is fairly simple. The disclosure indicator should always be used to indicate that another screen would be displayed if the cell were selected (i.e. the user taps anywhere on the cell). The disclosure button should be used to indicate that another screen would be displayed only when the button is tapped.

Nicolaas tenBroek (AMDG)

117

iOS Development

Tables

Next we will discuss the two protocols: UITableViewDataSource and UITableViewDelegate. The class that either contains or is the data model for the table must implement the UITableViewDataSource protocol. This protocol defines methods for indicating the number of sections in the table, the number of rows in each section, and the cell to be displayed in each row. Additional methods provide labels for the headers and footers of the different sections as well as support for editing the data in a cell. The UITableViewDelegate contains methods for informing observers of user interaction with the table. For instance, you can find out when an accessory button has been tapped, when a row has been selected, or even when a user wants to move a row from one location to another. Rather oddly, the delegate also contains methods for configuration of the header and footer views of each section and the height and indentation level of each row. The last class we need to know about for handling tables is the UITableViewController. This class combines everything you need for managing tables into one neat package. It is mostly a convenience class, but its use can save you some typing. UITableViewController extends UIViewController and implements both the UITableViewDataSource and UITableViewDelegate protocols. If you want to use this class, simply extend it, write the required methods from the UITableViewDataSource and add whichever additional methods you would like. While this is merely a convenience class, and does not provide any abilities over classes you might write yourself, you do need to be aware of it. All of the prebuilt table templates do use it, so you need to know why it exists and how to use it. We will explore examples of building table-based applications both with and without this class so that you can easily compare the two approaches. Now that we know the names and functions of the classes and protocols involved in making tables work, we shall start building some examples. For our first example we will implement the table manually rather than using one of the pre-built templates. Begin by creating a Single View Application for the iPhone platform without the Storyboard, but with ARC (we named ours BasicTable). In the ViewControllers header file we will need to indicate that we are implementing both the UITableViewDataSource and UITableViewDelegate protocols. We will also add an IBOutlet for the UITableView. Putting all that together gives us the following header file:
ViewController.h #import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> @property (nonatomic, weak) IBOutlet UITableView *table; @end

Next we need to edit the nib file (BasicTableViewController.xib) in Interface Builder. We will need to drag a UITableView component onto our existing UIView. Remember that all UI elements extend UIView, so technically the table itself could be the main view, but we will occasionally want to add a

Nicolaas tenBroek (AMDG)

118

iOS Development

Tables

table to an existing view and have it appear with other items. In this case, we will just take the simple route and add the table directly on top of the view. First, grab the table view from the list,

and then drag it onto the view. Notice that the table will automatically re-size itself to take up the entire view.

While we want the table to take up the entire view for this example, you may not want that behaviour in the future. There are two ways to handle this and the other controls that auto-size themselves. The first way is to prevent the auto sizing from obscuring the entire view. You can do this by dropping the component somewhere on the view other than in the top left corner. You will then be able to resize the component by grabbing an edge and dragging it to a new size. The second approach is to use the size inspector to change the size of the component. With the size inspector you can specify the width and height of almost any component, though a few components are not resizable, while some allow resizing along one axis and not the other. Now that we have the table on our view, wire up the IBOutlet from Files Owner, and the data source and delegate properties of the table (right click on the table and connect those elements to Files Owner). Failure to wire-up the data source and delegate from the table will result in an empty table at run-time. You should save your work at this point. Do not build yet as we are not quite ready for that step considering we have yet to implement the two required methods from the UITableViewDataSource. We also need to create our data model. For those items we will need to open our Controllers .m file.

Nicolaas tenBroek (AMDG)

119

iOS Development ViewController.m (Section 1 of 3)


#import "ViewController.h" @implementation ViewController { @private NSArray *tableData; } @synthesize table;

Tables

There is quite a bit going on in this file, so we will walk through the code one section at a time. First lets have a look at the table property. We really do not need it yet, but there are many times when you will need access to the table outside of the delegate and data source methods (for instance, if the data in the model changes). Having an IBOutlet is the only way to notify the table of such changes. Creating it also gives us some good practice with Interface Builder. We will also need a data model, and the array serves perfectly for simple tables. ViewController.m (Section 2 of 3)
#pragma mark - UITableViewDataSource Methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [tableData count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *reuseIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; if(!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; } cell.textLabel.text = [tableData objectAtIndex:indexPath.row]; return cell; } #pragma mark - UITableViewDelegate Methods //we will add these in a bit

Next we provide the two required methods from the UITableViewDataSource protocol. We begin by informing the table about the number of rows in each section of the table through the method tableView:numberOfRowsInSection:. Remember that the protocol is designed to support

Nicolaas tenBroek (AMDG)

120

iOS Development

Tables

modularisation of the components, so each method will include an argument referencing the table requesting the data. If our code were designed to support multiple tables we might need access to the specific table requesting the data. As our code is simple, we can ignore the argument. Additionally, the table defaults to one section, so we can ignore the section argument as well. In a multi-sectioned table you would use the section argument as an index into your data model. Our simple table will contain one row for each element in our array, so we return a count of the elements in the array. The tableView:cellForRowAtIndexPath: method is a bit more complicated. This methods job is to create a UITableViewCell and populate it for display in the indicated row of the indicated section of the table. This method is called each time a cell needs to be displayed on the screen, which is quite often if the table is scrolling. The section and row information are packaged together in the indexPath argument. The argument is an object of type NSIndexPath, which is designed to represent one particular cell in a nested array. The UITableView actually creates a category for NSIndexPath that reveals the section and row properties, which then allow us to discover which row in which section the cell will be displayed. The first line of code in our method creates a static string called reuseIdentifier and initialises that variable to @Cell. You will recall that declaring a local variable as static means it will only be created the first time the method is called. The variable will then remain in memory retaining its value across method calls. This particular method is called many times and often in quick succession, so keeping variable creation to a minimum will greatly improve the performance of our method and thus the performance of the entire table. The UITableView control is actually quite efficient with the way it manages both memory and performance. The data model for a table could contain many thousands of rows, but only a few could ever be displayed at any given time. To maximise performance and to minimise the memory used by the graphical controls, the table queues UITableViewCell objects and allows the cells to be reused when their content has been scrolled off the screen. Tables are capable of displaying multiple kinds of cells at the same time, so the reuse identifier is used by the table as a key for the queue of a particular kind of cell. Each kind of cell you want displayed in your table will need its own unique reuse identifier. As we are only using one kind of cell in our table, we need only one reuse identifier. The very next line of code demonstrates this queue in action. Here we ask the table to de-queue a reusable cell. If a cell is available for use then it will be returned, if not the method will return nil. The following if statement checks to see whether we received a cell or nil. If we received nil then we must create a new cell. In our creation step we assign the new cell the reuse identifier and the style of cell we would like. For our example we used the default cell style that you will recall contains an optional image and a single black left-aligned label. Finally, we populate the cells label with the text for a given row, and then return the cell. Our example is quite simple, so it is easy to overlook this, but you must remember that the cells can be reused. That means in this section of code you must set all of the cells properties that could have been set in previous method calls. If you have items that could be optionally displayed it is not good enough to only

Nicolaas tenBroek (AMDG)

121

iOS Development

Tables

set those items when they need to be displayed. You would also need to explicitly turn them off it they need to not be displayed. Notice also that we have used the #pragma pre-processor directives to divide our code. This will help us to keep track of where things came from, and is only for organisational purposes. It is a very good coding practice to add the pragma directives in as soon as you start editing a file, as they will help keep the file organised. They also help make the navigation within XCode easier, which will become important when your files grow to real-world lengths. At the moment our UITableViewDelegate section is actually empty. We will be writing methods from that protocol a bit later, but we wanted to plan for them now. The next section of code contains the standard ViewController methods. These are not specific to tables, but are required in all Controllers. ViewController.m (Section 3 of 3)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle { if(self = [super initWithNibName:nibName bundle:nibBundle]) { tableData = [NSArray arrayWithObjects:@"Mercury", @"Gemini", @"Apollo", @"Space Shuttle", nil]; } return self; } @end

The methods in this section should be somewhat familiar as we used them in our previous example. Notice that in our initWithNibName:bundle: method we create and populate our array (this method is called for us in the App Delegate). For no particular reason we used the class method arrayWithObjects: to create the array. This method allocates and initialises the array, then places it in the auto release pool. In order for us to use it later we need to retain the reference. We finish our file with a bit of memory clean up code, and then save. After we compile and run we see the results of our effort: a nice clean table.

Nicolaas tenBroek (AMDG)

122

iOS Development

Tables

Nicolaas tenBroek (AMDG)

123

iOS Development

Tables

Creating a Table with Sections


The basic table display is very nice, but a bit plain, so before we get into the remaining delegate methods, we will make our app a bit more interesting. To do that we will need a more complex data model, which allows us to expand the data we already have with the missions from each of the space projects listed. We will do that by first creating a simple class to represent a space project. Each project will have a name, a starting and ending year, and an array of the missions that formed the project. Project.h
#import <Foundation/Foundation.h> @interface Project : NSObject @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, readonly) readonly) readonly) readonly) NSString *name; int yearBegan; int yearEnded; NSArray *missions;

- (id)initWithName:(NSString *)projectName yearBegan:(int)beginning yearEnded:(int)end andMissions:(NSArray *)projectMissions; @end

Note that we made the properties read-only. As the data is historical in nature, the values should not be changing once the object has been created. Therefore we have no need to expose the ability to set those values. Now lets examine the implementation of the project. Project.m
#import "Project.h" @implementation Project { @private NSString *name; int yearBegan; int yearEnded; NSArray *missions; } @synthesize @synthesize @synthesize @synthesize name; yearBegan; yearEnded; missions;

- (id)initWithName:(NSString *)projectName yearBegan:(int)beginning yearEnded:(int)end andMissions:(NSArray *)projectMissions { if(self = [super init]) { name = projectName; yearBegan = beginning; yearEnded = end;

Nicolaas tenBroek (AMDG)

124

iOS Development Project.m


} } - (NSString *)description { return [NSString stringWithFormat:@"%@ %d - %d", name, yearBegan, yearEnded]; } @end missions = projectMissions;

Tables

return self;

Taking a closer look in the initWithName:yearBegan:yearEnded:andMissions: method, we see that the data are assigned into the instance variables without using the self reference. The properties were created as read-only which means that we do not have synthesized setter methods capable of handling the assignment; therefore using the self reference would generate compile errors. You will recall that the description method is inherited from NSObject, so there is no need to prototype it in the header file, we need only override it. Next, we will create a class to hold the basic information about a projects mission. Again we will make the properties read-only because they should not be changed after the object has been created. Mission.h
#import <Foundation/Foundation.h> @interface Mission : NSObject @property @property @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, readonly) readonly) readonly) readonly) readonly) readonly) readonly) NSString NSString NSString NSString NSString NSString NSString *name; *rocket; *missionID; *missionDates; *launchTime; *duration; *notes;

- (id)initWithName:(NSString *)missionName rocket:(NSString *)rocketName missionID:(NSString *)mID missionDates:(NSString *)mDate launchTime:(NSString *)lTime duration:(NSString *)flightDuration andNotes:(NSString *)missionNotes; @end

Nicolaas tenBroek (AMDG)

125

iOS Development Mission.m


#import "Mission.h" @implementation Mission { @private NSString *name; NSString *rocket; NSString *missionID; NSString *missionDates; NSString *launchTime; NSString *duration; NSString *notes; } @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize name; rocket; missionID; missionDates; launchTime; duration; notes;

Tables

- (id)initWithName:(NSString *)missionName rocket:(NSString *)rocketName missionID:(NSString *)mID missionDates:(NSString *)mDate launchTime:(NSString *)lTime duration:(NSString *)flightDuration andNotes:(NSString *)missionNotes { if(self = [super init]) { name = missionName; rocket = rocketName; missionID = mID; missionDates = mDate; launchTime = lTime; duration = flightDuration; notes = missionNotes; } } return self;

- (NSString *)description { return [NSString stringWithFormat:@"%@ launched with: %@ mission ID: %@ dates: %@ at %@ flew for: %@ mission notes: %@", name, rocket, missionID, missionDates, launchTime, duration, notes]; } @end

Now that we have some simple classes to represent a project and its missions, we will go back into our tables ViewController and modify our Model. We will replace the simple array of strings with an array of projects. Each project contains an array of missions, so this arrangement gives us a two-dimensional

Nicolaas tenBroek (AMDG)

126

iOS Development

Tables

array of arrays, which will allow us to demonstrate the section property of tables. We are not modifying the ViewController.h, so only the .m file will be presented. ViewController.m (section 1 of 2)
#import "ViewController.h" #import "Project.h" #import "Mission.h" @implementation ViewController { @private NSArray *tableData; } @synthesize table; #pragma mark - UITableViewDataSource Methods - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [tableData count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[tableData objectAtIndex:section] missions] count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [[tableData objectAtIndex:section] description]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *reuseIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; if(!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; } cell.textLabel.text = [[[[tableData objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row] name]; } return cell;

#pragma mark - UITableViewDelegate Methods //we will add these in a bit

Nicolaas tenBroek (AMDG)

127

iOS Development

Tables

Here you can see that we have made some interesting changes that while fairly simple, will have a dramatic impact on the appearance of the table. First, take a look at the new method numberOfSectionsInTableView:. Here we return the count of the array (or in other words, the number of projects). This is exactly the code we had in tableView:numberOfRowsInSection: previously. Now that our data has two dimensions, we need the array count to serve as the count of the first dimension (Projects). The second dimension (Missions) will now be handled in tableView:numberOfRowsInSection:. For each section, we access the project at that index and then return the count of missions in that project. These two changes are all that is required to create a table with multiple sections in which each section can have a different number of rows. Next we add another new method called tableView:titleForHeaderInSection:. As its name implies, this method returns a string to be used as the title for a particular section. While this method is not required, had we left it out the table would be displayed with no visual separation between the sections. In this case we are returning the projects description as the section title. The next change we need to make is a very slight modification to tableView:cellForRowAtIndexPath:. Previously we used the row property of the indexPath as an index into our array. As we now have two dimensions of data we will need to also use the section property. As with the earlier methods, the section property serves as the index of a particular project. The row property now serves as the index for a particular mission. Once we have that mission we put its name into the cells text label. No other changes to this method are required. Finally, we need to input data for our table. This next piece of code is quite a bit longer than it was in the previous example, but that extra length is entirely data. In the interest of brevity we have created objects for the first five missions of each project, rather than a comprehensive exploration. Sources are noted in the comments above each project. The code here is voluminous, but simple. We create five missions, and then use those missions to create a project. Once we have created several projects we place those projects into our data array. ViewController.m (section 2 of 2)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle { if(self = [super initWithNibName:nibName bundle:nibBundle]) { //"Project Mercury." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Mercury>. Mission *mercury1 = [[Mission alloc] initWithName:@"Little Joe 1" rocket:@"Little Joe" missionID:@"LJ-1" missionDates:@"21 August 1959" launchTime:@"N/A" duration:@"00d 00h 00 m 20s" andNotes:@"Test of launch escape system during flight."]; Mission *mercury2 = [[Mission alloc] initWithName:@"Big Joe 1" rocket:@"Atlas 10-D" missionID:@"Big Joe 1" missionDates:@"9 September 1959" launchTime:@"N/A" duration:@"00d 00h 13 m" andNotes:@"Test of heat shield and Atlas / spacecraft interface."]; Mission *mercury3 = [[Mission alloc] initWithName:@"Little Joe 6" rocket:@"Little Joe" missionID:@"LJ-6" missionDates:@"4 October 1959" launchTime:@"N/A" duration:@"00d 00h 05 m 10s" andNotes:@"Test of spacecraft aerodynamics and integrity."]; Mission *mercury4 = [[Mission alloc] initWithName:@"Little Joe 1A" rocket:@"Little Joe"

Nicolaas tenBroek (AMDG)

128

iOS Development ViewController.m (section 2 of 2)

Tables

missionID:@"LJ-1A" missionDates:@"4 November 1959" launchTime:@"N/A" duration:@"00d 00h 08 m 11s" andNotes:@"Test of launch escape system during flight."]; Mission *mercury5 = [[Mission alloc] initWithName:@"Little Joe 2" rocket:@"Little Joe" missionID:@"LJ-2" missionDates:@"4 December 1959" launchTime:@"N/A" duration:@"00d 00h 11 m 06s" andNotes:@"Carried Sam the monkey to 85 kilometres in altitude."]; Project *mercury = [[Project alloc] initWithName:@"Mercury" yearBegan:1959 yearEnded:1963 andMissions:[NSArray arrayWithObjects:mercury1, mercury2, mercury3, mercury4, mercury5, nil]]; //"Project Gemini." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Gemini>. Mission *gemini1 = [[Mission alloc] initWithName:@"Gemini 1" rocket:@"Titan II" missionID:@"GLV-1 12556" missionDates:@"812 April 1964" launchTime:@"16:01 UTC" duration:@"03d 23h" andNotes:@"First test flight of Gemini"]; Mission *gemini2 = [[Mission alloc] initWithName:@"Gemini 2" rocket:@"Titan II" missionID:@"GLV-2 12557" missionDates:@"19 January 1965" launchTime:@"14:03 UTC" duration:@"00d 00h 18m 16s" andNotes:@"Suborbital flight to test heat shield"]; Mission *gemini3 = [[Mission alloc] initWithName:@"Gemini III" rocket:@"Titan II" missionID:@"GLV-3 12558" missionDates:@"23 March 1965" launchTime:@"14:24 UTC" duration:@"00d 04h 52m 31s" andNotes:@"First manned Gemini flight, three orbits."]; Mission *gemini4 = [[Mission alloc] initWithName:@"Gemini IV" rocket:@"Titan II" missionID:@"GLV-4 12559" missionDates:@"37 June 1965" launchTime:@"15:15 UTC" duration:@"04d 01h 56m 12s" andNotes:@"Included first extravehicular activity (EVA) by an American; White's \"space walk\" was a 22 minute EVA exercise."]; Mission *gemini5 = [[Mission alloc] initWithName:@"Gemini V" rocket:@"Titan II" missionID:@"GLV-5 12560" missionDates:@"2129 August 1965" launchTime:@"13:59 UTC" duration:@"07d 22h 55m 14s" andNotes:@"First week-long flight; first use of fuel cells for electrical power; evaluated guidance and navigation system for future rendezvous missions. Completed 120 orbits."]; Project *gemini = [[Project alloc] initWithName:@"Gemini" yearBegan:1964 yearEnded:1966 andMissions:[NSArray arrayWithObjects:gemini1, gemini2, gemini3, gemini4, gemini5, nil]]; //"Project Apollo." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Apollo>. //"AS-201." Wikipedia. Web. 29 November 2010. <http://en.wikipedia.org/wiki/AS-201>. Mission *apollo1 = [[Mission alloc] initWithName:@"AS-201" rocket:@"Saturn 1B" missionID:@"AS-201" missionDates:@"26 February 1966" launchTime:@"16:12:01 UTC" duration:@"37 min 19.7 s" andNotes:@"Partial Success - Unmanned suborbital flight was the first test flight of Saturn 1B and of the Apollo Command and Service Modules; problems included a fault in the electrical power system and a 30 percent decrease in pressure to the service module engine 80 seconds after firing."]; //"AS-203." Wikipedia. Web. 29 November 2010. <http://en.wikipedia.org/wiki/AS-203>. Mission *apollo2 = [[Mission alloc] initWithName:@"AS-203" rocket:@"Saturn 1B" missionID:@"AS-203" missionDates:@"5 July 1966" launchTime:@"14:53:13 UTC"

Nicolaas tenBroek (AMDG)

129

iOS Development ViewController.m (section 2 of 2)

Tables

duration:@"~6 hours" andNotes:@"Success - fuel tank behavior test and booster certification - informally proposed later as Apollo 2, this name was never approved."]; //"AS-202." Wikipedia. Web. 29 November 2010. <http://en.wikipedia.org/wiki/AS-202>. Mission *apollo3 = [[Mission alloc] initWithName:@"AS-202" rocket:@"Saturn 1B" missionID:@"AS-202" missionDates:@"25 August 1966" launchTime:@"17:15:32 UTC" duration:@"1 h 33 min 2 s" andNotes:@"Success - command module reentry test successful, even though reentry was very uncontrolled. Informally proposed as Apollo 3, this name was never approved."]; //"Apollo 1." Wikipedia. Web. 29 November 2010. <http://en.wikipedia.org/wiki/Apollo_1>. Mission *apollo4 = [[Mission alloc] initWithName:@"AS-204" rocket:@"Saturn 1B" missionID:@"Apollo 1" missionDates:@"21 February - 7 March 1967 (planned)" launchTime:@"(Launch cancelled)" duration:@"Up to 14 days (planned)" andNotes:@"Failure - never launched: command module destroyed and three astronauts killed on 27 January 1967 by fire in the module during a test exercise - Retroactively, the mission's name was officially changed to \"Apollo 1\" after the fire. Although it was scheduled to be the fourth Apollo mission (and despite the fact that NASA planned to call the mission AS-204), the flight patch worn by the three astronauts, which was approved by NASA in June 1966, already referred to the mission as \"Apollo 1\""]; //"Apollo 4." Wikipedia. Web. 29 November 2010. <http://en.wikipedia.org/wiki/Apollo_4>. Mission *apollo5 = [[Mission alloc] initWithName:@"Apollo 4" rocket:@"Saturn V" missionID:@"Apollo 4" missionDates:@"9 November 1967" launchTime:@"12:00:01 UTC" duration:@"8 h 36 m 59 s" andNotes:@"Success - first test of new booster and all elements together (except lunar module), successful reentry of command module"]; Project *apollo = [[Project alloc] initWithName:@"Apollo" yearBegan:1966 yearEnded:1975 andMissions:[NSArray arrayWithObjects:apollo1, apollo2, apollo3, apollo4, apollo5, nil]]; } } @end tableData = [NSArray arrayWithObjects:mercury, gemini, apollo, nil];

return self;

Now, when we run our app, we see a fairly nice display that is quite a bit more interesting than the last.

Nicolaas tenBroek (AMDG)

130

iOS Development

Tables

This particular display style is one of the two available for tables with sections. The style we have used (and is the default) is called Plain while the other style is Grouped. To change to a grouped display, we need to edit our nib file. Select the table from the View and display the Attributes Inspector. Under the section labelled Table View you will see a drop down labelled Style. Choose Grouped from the drop down.

With that one change made we can save, build, and run the app. The impact of the style choice is rather dramatic (though not necessarily better). Grouped tables do not require section titles to maintain visual separation between sections the way a plain table does, which means the titles are optional in this layout.

Nicolaas tenBroek (AMDG)

131

iOS Development

Tables

Nicolaas tenBroek (AMDG)

132

iOS Development

Tables

Displaying a Table Index


As it stands our table is pretty short, and navigating from top to bottom takes less than a second. If we started adding more data though, the table could quickly become very long. Even in the restricted example of space flight, a complete history would include hundreds of projects, some of which had dozens of missions. At that point the tables current navigation system (swiping with your finger to scroll) would become fairly annoying. Luckily the table control provides support for an additional navigation system. You can add what amounts to a table of contents (no pun intended) that will be displayed on the right side of the screen and provide instant access to different sections. The table of contents is referred to as a section index in Objective-C. While that title seems straightforward enough, it leads to a rather unfortunate situation where the strings displayed on the side are referred to as section index titles. Despite its somewhat odd name, the index titles are not required to be a replication of the section titles. Typically the index titles are very short so as to take up as little screen space as possible while still being useful. There are essentially two ways to implement the section index, one simple, and one slightly more complex. We will start with the simple method, as our data is very simple. First, we will need to add an additional array to support the index, so we need to modify our View Controllers source file. Rather than reproducing the entire file, only the changes will be listed. ViewController.m (section 1 of 3)
#import "ViewController.h" #import "Project.h" #import "Mission.h" @implementation ViewController { @private NSArray *tableData; NSMutableArray *indexTitles; } @synthesize table;

We begin our source code changes by adding an additional array (in bold above). We made this array mutable because we will be calculating its contents at run time. ViewController.m (section 2 of 3)
#pragma mark - UITableViewDataSource Methods - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return indexTitles; }

Next we a new data source method called sectionIndexTitlesForTableView. This method needs only to return the array of NSString objects that will be used as the titles.

Nicolaas tenBroek (AMDG)

133

iOS Development

Tables

Finally we need to modify initWithCoder: to create and populate our indexTitles array. To keep our example as simple as possible, we are reusing the project names as index titles. As the names are a bit long this would not be the best practice in the real world. ViewController.m (section 3 of 3)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle { if(self = [super initWithNibName:nibName bundle:nibBundle]) { /* .............. DATA CREATION REMOVED FOR BREVITY .............. */ indexTitles = [[NSMutableArray alloc] initWithCapacity:[tableData count]]; for(Project *project in tableData) { [indexTitles addObject:project.name]; } } } return self;

With those few changes in place our table now displays with an index on the right.

Nicolaas tenBroek (AMDG)

134

iOS Development

Tables

Note: You likely noticed that we changed our table style back to Plain. While the Index will work with grouped tables in later versions of iOS, it did not in earlier versions. Regardless of the functionality, it can be very hard for some people to see the Index on grouped tables, and it tends to make the display look both disjointed and cluttered. It is best practice to only use an Index with a Plain style table. Tapping on any of the names on the right will instantly scroll to the section that matches. This works at the moment because we have a one-to-one relationship of index entries to table sections and the contents of the index are listed in exactly the same order as the data in the table. If we used some other indexing system (the entre alphabet for instance) this simple approach would not work. Notice that the display of the index is automatically distributed to take up the entire screen. This distribution does not inform us about the amount of data in each section; it is simply evenly distributed in the space provided by the screen. Right now, that index is both informative and useful. If we were to make the list of projects comprehensive, listing the title of each project in the index would quickly become unusable. In fact, there will certainly be more project names than will fit on the screen as an index. Such a situation would force us to use an alternate approach. We may decide to organise our data by decade, alphabetically, by country of origin, or some other mechanism. Whichever ordering we decide to use, the index would need to reflect that organisation, and would no longer have a one-to-one relationship of index-entry to table section. For instance, if we were to use the letters of the alphabet as our index titles, we may well have several projects that all begin with the same letter and possibly some letters with no associated project at all. This means we would need some way to map the letter to only the first section with that letter, and some way to handle letters with no matching projects. Handling such a setup requires a bit more effort and an additional method. Before we can get to the work of coding, we need to modify the Controllers data a bit to hold the mapping. ViewController.m (section 1 of 3)
#import "ViewController.h" #import "Project.h" #import "Mission.h" @implementation ViewController { @private NSArray *tableData; NSArray *indexTitles; NSMutableArray *titleToSectionMap; } @synthesize table;

As you can see, the changes required were not drastic. We converted our indexTitles from a mutable array to an immutable one (we will be hard-coding its contents because the alphabet seems pretty stable at this point), and we have added a new mutable array for mapping Index titles to table sections.

Nicolaas tenBroek (AMDG)

135

iOS Development ViewController.m (section 2 of 3)


#pragma mark - UITableViewDataSource Methods

Tables

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { return [[titleToSectionMap objectAtIndex:index] intValue]; }

Next we have added an additional method with a somewhat confusing name: tableView:sectionForSectionIndexTitle:atIndex:. Remember that the section index title is the title from the index (or table of contents). The atIndex: argument is the index into the array of titles. So this method receives two arguments that both tell you about what the user tapped. We receive the string the user tapped and the location of that string in the array of titles. We need to take that data and convert it to an index of a section in the table. As we have created (or rather, will soon create) a mapping of Index indices to table sections, we can do just that by simply looking up the right spot in the mapping array. NSArrays can only store objects, and the return value of the method is an int, so we must ask the object for its integer value by calling the method intValue. Finally, in the initWithCoder: method we create our Index titles. Our Index will simply be the alphabet, so we can hard code those strings. With that accomplished we can loop through the Index titles and the table sections, looking for the first item in our table sections which begins with the same letter as the Index title. Once we have found a section title that matches we can store the index of that table section in our mapping. If no matching section is found, then the index value we store for that letter is -1. ViewController.m (section 3 of 3)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle { if(self = [super initWithNibName:nibName bundle:nibBundle]) { /* .............. DATA CREATION REMOVED FOR BREVITY .............. */ indexTitles = [NSArray arrayWithObjects:@"A", @"B", @"C", @"D", @"E", @"F", @"G", @"H", @"I", @"J", @"K", @"L", @"M", @"N", @"O", @"P", @"Q", @"R", @"S", @"T", @"U", @"V", @"W", @"X", @"Y", @"Z", nil]; titleToSectionMap = [[NSMutableArray alloc] initWithCapacity:[indexTitles count]]; NSRange range; range.location = 0; range.length = 1; for(int i = 0; i < [indexTitles count]; i++) {

Nicolaas tenBroek (AMDG)

136

iOS Development ViewController.m (section 3 of 3)

Tables

NSString *title = [indexTitles objectAtIndex:i]; int index = -1; for(int j = 0; j < [tableData count]; j++) { if([[[tableData objectAtIndex:j] name] compare:title options:NSCaseInsensitiveSearch range:range] == NSOrderedSame) { index = j; break; } } [titleToSectionMap addObject:[NSNumber numberWithInt:index]]; } } }

return self;

Running our app we see the fruits of our labour.

Notice that if you tap a letter in the Index that is unused (i.e. we have no matching section), nothing happens. This is because of the decision we made to store the value -1 for those Index entries. This was not the only decision possible in this situation, and not necessarily the best one either. For instance, we could have stored a value of 0, which would cause the first section to be displayed for any non-used Index entry. We could have also have computed the index of the closest matching section for that letter. Think carefully before using -1 as an Index to section mapping. If you have a preponderance of

Nicolaas tenBroek (AMDG)

137

iOS Development non-working Index entries as we do in this example, the user may conclude that the Index is nonfunctional or only functions erratically.

Tables

Nicolaas tenBroek (AMDG)

138

iOS Development

Tables

UITableViewDelegate
Now that we have a functional table populated with data, it would be a good time to look at the UITableViewDelegate protocol. The table view delegate is useful for managing a large range of tasks needed for advanced tables. For instance, it can help you manage the configuration of rows to display cells that are different heights in the same table, or the indentation levels of cells for a nested data view. You can use it to manage the header and footer displays of sections if you need something more than the standard labelling we used in the earlier examples. You can also use it to manage user selections, editing, and even reordering of rows. We will start our examination with a simple example of responding to row selections. We can use the users selection of a mission to display some additional data already present in our model. To process selection events we can use the method tableView:didSelectRowAtIndexPath: which is called after a selection has completed. There are also methods that will be called before a selection occurs (after the user taps the row, but before the table marks it as selected), and both before and after a row has been deselected. That collection of methods offers a great deal of control over how and when the response to selection happens. In our new method we retrieve the selected mission and use it to populate a pop-up dialogue. BasicTableViewController.m
#pragma mark - UITableViewDelegate Methods - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Mission *mission = [[[tableData objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:mission.name message:[mission description] delegate:nil cancelButtonTitle:@"Close" otherButtonTitles:nil]; [alert show]; }

This is a very small change, but it is all we need. Save, build, run, and then select a row. The display should change to be something like the following screen shot:

Nicolaas tenBroek (AMDG)

139

iOS Development

Tables

The UIAlertView will automatically change its display based on the amount of information being presented. For instance, if we select the first Apollo mission we see that the dialogue now contains a UITextView:

Of course, the formatting and display of the data is not what one would consider good as we simply jammed all of our information into one long string. A much better approach would be to create a screen designed specifically for properly displaying all of a missions information. That kind of setup is a bit beyond our scope at the moment, but it will be covered in the chapter on navigation

Nicolaas tenBroek (AMDG)

140

iOS Development

Tables

Editing
Many tables allow modification of the data. Currently ours does not (and one could easily argue that perhaps it should not), but enabling that functionality will not prove too challenging. Tables offer reordering of rows, row deletion, and row insertion as possible editing options. While you might not want all of those options on every project, they are all easy to implement, and you can decide which behaviours you would like to support based on your needs at the time. As revisionist history is all the rage, we thought we could participate in it by allowing users to move missions into different projects so they can match whatever whims strike at the time. To support editing, we will need to modify our Project class, add a control to our view, and modify the ViewController class. None of the modifications will be difficult or complicated, but we will take them one step at a time to keep confusion to a minimum. We shall begin in the Projects header file by importing the Mission header in support of our new methods. Then add a method for removing a mission and another for inserting a mission. Project.h
#import <Foundation/Foundation.h> #import "Mission.h" @interface Project : NSObject @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, readonly) readonly) readonly) readonly) NSString *name; int yearBegan; int yearEnded; NSArray *missions;

- (id)initWithName:(NSString *)projectName yearBegan:(int)beginning yearEnded:(int)end andMissions:(NSArray *)projectMissions; - (void)removeMissionAtIndex:(int)index; - (void)insertMission:(Mission *)mission atIndex:(int)index; @end

In a moment we will modify the source file to support the changes, but before we do we should consider the contents of the header for a moment. One of the changes we will make is to change the array of missions to a mutable array. Notice that neither the missions property statement nor the missions argument in the init method reflects this fact. We easily could modify those two items to indicate the modifiable nature of the array, but doing so would violate the implementation hiding rules of OOP. Keeping the nature of the array hidden offers a modicum of data protection, but also means that changes made here will have no impact on other files within the project (which is one of the goals of implementation hiding). You may be curious about the property statement. If the actual array we are using is mutable, why would the property show it as immutable? This works because (oddly enough) NSMutableArray is a subclass NSArray, which means the two are essentially the same type of object. We have no need to allow direct modification of the array through the property (i.e. outside of our

Nicolaas tenBroek (AMDG)

141

iOS Development

Tables

control), so keeping the propertys type as NSArray affords us an amount of data protection, small though it may be at this point. The safest option would be to write our own getter method and return a copy of the array, but we do not need to go that far for this example. Project.m
#import "Project.h" @implementation Project { @private NSString *name; int yearBegan; int yearEnded; NSMutableArray *missions; } @synthesize @synthesize @synthesize @synthesize name; yearBegan; yearEnded; missions;

- (id)initWithName:(NSString *)projectName yearBegan:(int)beginning yearEnded:(int)end andMissions:(NSArray *)projectMissions { if(self = [super init]) { name = projectName; yearBegan = beginning; yearEnded = end; missions = [NSMutableArray arrayWithArray:projectMissions]; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %d - %d", name, yearBegan, yearEnded]; } - (void)removeMissionAtIndex:(int)index { [missions removeObjectAtIndex:index]; } - (void)insertMission:(Mission *)mission atIndex:(int)index { [missions insertObject:mission atIndex:index]; } @end

Rather interestingly, we have made only small changes to the implementation file. First, we changed the array assignment statement in the init method. Notice that we created a new mutable array with a

Nicolaas tenBroek (AMDG)

142

iOS Development copy of the data we received. Next we defined our two new methods for inserting and removing missions.

Tables

Now that the projects are able to process modification of their mission arrays, it is time to modify our Controller to support table editing. The UITableView can only be edited while it is in the editing state. The editing state is something that the table should transition into for editing, and then out of for normal operations. Therefore we need some mechanism for the user to indicate that they are ready to edit, allowing us to transition the table into an editing state. That mechanism is independent of the table itself, which means you are free to choose whatever design fits your current needs. Right now our user interface is quite simple, so a UIButton would work well. For this particular task we will need to add both an IBOutlet and an IBAction to support the button. Normally, an IBAction would be enough to handle the button press, but we will need to modify our button each time it is pressed to reflect the current editing state of the table, and to do that safely we need an IBOutlet. We will begin as always in the header file, adding only the new property and method. We will name our new IBAction method toggleEditMode to remind us that the editing state is something the table will transition into and out of. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> @property (nonatomic, weak) IBOutlet UITableView *table; @property (nonatomic, weak) IBOutlet UIButton *editButton; - (IBAction)toggleEditMode; @end

Note: It may occur to you that we could have dispensed with the buttons property and simply used the action method signature that includes a reference to the control which caused the action. You would be correct in that assessment. However, you must keep in mind that projects grow over time, and it is entirely possible that at some later date multiple controls might call our action method, they might not even all be buttons. So, to be absolutely sure of what the app is doing we will only access the controls directly. That will keep our code stable over time, which will ultimately mean much less work for us, and anyone else that modifies our code. Now we can move to the source file for the real work. As before, we will only list the new and changed methods here for the sake of brevity. ViewController.m (section 1 of 2)
#pragma mark - UITableViewDataSource Methods - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return YES;

Nicolaas tenBroek (AMDG)

143

iOS Development ViewController.m (section 1 of 2)


}

Tables

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { Project *fromProject = [tableData objectAtIndex:fromIndexPath.section]; Project *toProject = [tableData objectAtIndex:toIndexPath.section]; [toProject insertMission:[fromProject.missions objectAtIndex:fromIndexPath.row] atIndex:toIndexPath.row]; [fromProject removeMissionAtIndex:fromIndexPath.row]; }

Our first new method is tableView:canMoveRowAtIndexPath:. The table uses this method to ask if a specific row is eligible for moving. Returning YES for a given row means the row will display a move icon when the table is in edit mode, and will respond to move gestures (drag and drop). Returning NO means the given row will not display the move icon and will not respond to the move gestures. The rows default to NO, so if we do not implement this method no rows will be able to move. In our simple example all rows will be eligible for moving, so we simply return YES, ignoring the index path variable. The next new method is tableView:moveRowAtIndexPath:toIndexPath:. This is where the real work of moving the rows happens, and as you can see, is not terribly difficult to implement. We simply retrieve the projects located at both the fromIndexPath and toIndexPath. Then we insert the mission into the new project, and finally, remove it from the old project. The order of steps here is important, so be careful! If we removed the mission from the old project first, the remove operation from the array would call the release method on the mission. The missions retain count while it is in the array is one, so that call to release would signal that the memory is ready to return to the system, which then invalidates the object. By adding the mission to its new project first the retain count goes up to two, then the remove process decrements it back to one. Memory handling is something you must always keep in mind when working in Objective-C! The observant reader will likely think here that we could have easily avoided this issue by making the method that removes the mission from the array return the mission, and thus reduced the number of lines necessary to move the mission to one. Indeed that would work, but it would also force the ARC to add the removed mission to an auto-release pool, which would likely affect the lifespan of the object and the memory it consumes. While not an issue in this project, in a larger project such a change could have dramatic impacts. Note that both of these methods are housed in the UITableViewDataSource protocol. While a user moving rows around the screen may seem more like a task suited to the delegate, the impact of those actions is that the data model must change. The data source is responsible for the model, and thus the methods are housed in that protocol.

Nicolaas tenBroek (AMDG)

144

iOS Development

Tables

ViewController.m (section 2 of 2)
#pragma mark - IBActions - (IBAction)toggleEditMode { [table setEditing:!table.editing animated:YES]; if(table.editing) { [editButton setTitle:@"Done" forState:UIControlStateNormal]; } else { [editButton setTitle:@"Edit" forState:UIControlStateNormal]; } }

Our next task is to implement the new action method toggleEditMode. The first thing this method does is to set the tables editing mode to the opposite of its current state. We used YES for the animated argument so that the new icons appear and disappear with a smooth transition. Next we change the title of the button so that it displays an appropriate message to the user depending on the state of the table. With the code modifications complete we can move on to editing the nib file where we need to add a button to the view. At the moment though, the table covers up the entire view leaving no room for other controls. Recall that most controls can be re-sized by grabbing the light blue dot at one of the edges and then dragging along either the vertical or horizontal axis depending on which side you are resizing. You can also use the Size Inspector to set the size to an explicit pixel value and then place it on the screen by specifying a location for the controls top-left corner. As our example does not need to be perfect (or pretty), we will opt for dragging the top of the table down and approximate the space needed for the button.

Nicolaas tenBroek (AMDG)

145

iOS Development Next, grab a UIButton from the list

Tables

And drag it over to the view. Place it somewhere within the space we just made by shrinking the table.

Double click the button to edit it, and give it the name Edit. Make sure you spell it the same way you did in your code, or the name will disappear forever the first time you click it (as it will be replaced with the text in your code), which would obviously leave our users a bit confused.

Dont forget to wire up the button! You will need to connect the IBOutlet and the IBAction. Remember also that the IBAction should be connected to Touch Up Inside. With those changes made save, compile, and run, and you will see something like this:

Nicolaas tenBroek (AMDG)

146

iOS Development

Tables

Next, press the edit button. The cells will smoothly animate into edit mode and the buttons title will change to Done.

Next, grab a row by the move icon (the three horizontal lines on the right) and move it around.

Nicolaas tenBroek (AMDG)

147

iOS Development

Tables

Drop the row into a new project, or even just into a new position within the same project.

Finally, press Done and the table transitions out of edit mode. Our button also renames itself to Edit to reflect the change. Thats it. We have created a table with rows that can be rearranged by the user.

Nicolaas tenBroek (AMDG)

148

iOS Development

Tables

Of course, the true test of the move comes after you have scrolled the moved rows off the screen. Remember that the table does not ask for the cells from the data source again until it needs to re-draw them. So scroll around and make sure things dont move into funny places or crash. If everything seems stable, then you have done a good job. We are not quite done though. You may have noticed the red and white icon on the left side of the cell while editing. It looks startlingly like a Do Not Enter sign, but is in fact the Delete icon.

Pressing the delete icon causes it to rotate and a Delete button to appear on the right.

The trouble is, our table does not support row deletion. It easily could though, because we have already modified our model to support removing missions. Implementing deletion would be literally trivial for us, but before we head down that path, lets see how we can get rid of the delete icon altogether. For that we will need to turn to the UITableViewDelegate again and implement the method tableView:editingStyleForRowAtIndexPath:. That method is called for each row when the table enters an editing state. We are treating all rows equally, so this method will be quite simple in our example.

Nicolaas tenBroek (AMDG)

149

iOS Development

Tables

We will ignore the index path argument and just return none for the editing style. We could use this method to implement more interesting editing scenarios though. For instance, we could return an editing style of insert for only the last row of each section to allow inserting new items at the end of each section. ViewController.m
#pragma mark - UITableViewDelegate Methods - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleNone; }

With that change in place, run the app again, then put the table into edit mode. Now our cells display the move icon only, which more accurately reflects the abilities of the code.

That change was easy enough, but covering up the problem hardly seems satisfying. Rather than stop at this point we should examine editing a bit further. There are in fact three editing styles for cells. Those are: UITableViewCellEditingStyleNone, which we just used, UITableViewCellEditingStyleDelete which we saw earlier and is the default, and UITableViewCellEditingStyleInsert which displays a green circle enclosing a white plus sign. If we decide we do want to implement deletion of rows, then we either need to remove the method we just added (tableView:editingStyleForRowAtIndexPath:), or change the return type to UITableViewCellEditingStyleDelete. After making either one of those changes we also need to add a data source method called tableView:commitEditingStyle:forRowAtIndexPath: to handle the actual deletion. ViewController.m
#pragma mark - UITableViewDataSource Methods - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if(editingStyle == UITableViewCellEditingStyleDelete) { [[tableData objectAtIndex:indexPath.section] removeMissionAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade]; } }

Nicolaas tenBroek (AMDG)

150

iOS Development

Tables

This method is called after the user interacts with the table to signal an edit decision. There are two possible values for the editingStyle argument: UITableViewCellEditingStyleDelete and UITableViewCellEditingStyleInsert. As we are only concerned with deletions, we will check for that editing style, and ignore inserts. Once we have determined the user pressed the delete button we ask the Project at indexPath.section to remove the Mission at indexPath.row. Then we ask the table itself to remove the row it has displayed. The tables method is set up to delete any number of rows in the same action, so we need to create an array containing the index path, and then pass it to the table. Notice we also indicate the animation style to use when deleting the row. There are a large number of animations to choose from including fade away (which we used), sliding off one of the four sides, collapsing into the middle, or no animation. Use whichever one fits the style of your application best. Note that you must commit the table animation only AFTER changing the model, as the table will immediately update the screen by re-processing the data model. If the models new size does not match the action taken by the table (deleting a row in this case), then the table will raise an exception and crash your app.

Nicolaas tenBroek (AMDG)

151

iOS Development

Tables

Custom Cells
At this point we have produced a rather decent table with a fair amount of interactivity. We have mostly ignored the appearance of the table though leaving it quite bland. Addressing the appearance problem requires that we look a bit more closely at the UITableViewCell. We already know that the cell supports several different layouts, but if none of those fit our needs then we are forced to build a custom one. There are several different approaches to cell customisation, but only one that follows the rules of Object Oriented Programming. One approach that has been popular for a while is to add a new nib file to the project, make a UITableViewCell the view, and then add additional controls to the cell identifying them only by changing the tag value. Then, within the tables controller, loading the nib and set the data in the cell by calling the method viewWithTag:. You may well come across this approach while looking at examples on the Internet, but it is rather backwards and should be avoided. This approach leads to code which cannot be read by humans (i.e. we lose the natural language we normally have) or even type checked by the compiler. That means there is no way to validate the code other than running it. It also means the compiler is unable to check and see if later changes made to the nib file will affect any class files. As an example of this approach, lets assume we created a nib file and added a UITableViewCell. Then we add three UILabels, giving them tag numbers 1, 2, and 3 in Interface Builder. Our code to populate those labels within tableView:cellForRowAtIndexPath: would look like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *reuseIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier]; if(!cell) { [[NSBundle mainBundle] loadNibNamed:@"MissionCell" owner:self options:nil]; cell = self.missionCell; } Mission *mission = [[[tableData objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row]; ((UILabel *)[cell viewWithTag:1]).text = mission.name; ((UILabel *)[cell viewWithTag:2]).text = mission.missionID; ((UILabel *)[cell viewWithTag:3]).text = mission.rocket; return cell; }

Notice that the viewWithTag: method returns only a UIView object, so we are forced to cast that return value to a UILabel. If we later change one of the components to another type there is no way for the compiler to test our code and let us know that we have an error. We will only find our error by crashing the app. The nib file also does not force tag numbers to be unique, so any mistake there (which is easily possible with a complex design) would produce inconsistent and hard to explain results. Nicolaas tenBroek (AMDG) 152

iOS Development

Tables

Finally, notice that when we loaded the cell we did not have the opportunity to tell the cell what the reuse identifier was. In fact, the identifier must be set through Interface Builder in the nib file. If we get the string wrong when asking the table to dequeue a cell, it will always return nil. Such a situation would produce excessive memory usage and a table with performance that will constantly decrease as the user scrolls around. There is a huge amount of risk associated in this design, and little if any payoff. At most it may save you two or three minutes of typing and will almost certainly set you up for hours of debugging later. Unless you really enjoy spending countless hours creating something that is inherently unstable, you should avoid this design approach. Of course, it is incredibly easy to implement a custom cell while following proper Object Oriented Design guidelines. In fact, we shall create a custom cell to display the missions name, ID, and rocket so the code can be directly compared directly to the bad approach. The first thing we will need to do is add a new class that extends UITableViewCell. We will need to add a class with a nib file, and while we can manually add a nib file at any time, it is usually easier to let the system help us. As before, we begin by right-clicking on the project folder and choosing New File from the pop-up menu.

We need a UIViewController subclass, so select that template.

Nicolaas tenBroek (AMDG)

153

iOS Development

Tables

Name the class MissionCell. Ensure that the With XIB for user interface checkbox is selected. If you forget that step you will need to manually add the nib file in later. Nib files can be found under the Resource heading from the left-side menu of the New File screen.

Press Create on the next screen and the files will be saved. Now that we have the files for our custom cell, we can write some code. The MissionCell class itself is very simple. We need only to create IBOutlets for the items we want to display and a string constant for our reuse identifier. Recall that Controllers loaded from nibs use the initWithCoder: method. This means we will not be allocating instances of our custom cell the same way we did previously when using UITableViewCell objects, and we will not have an opportunity to pass the reuse identifier in via the init method. That unfortunately puts us at risk for the same memory and performance problems we mentioned earlier if we accidentally use the incorrect identifier. Creating a string constant in this class can help us to avoid those issues though. MissionCell.h
#import <UIKit/UIKit.h> @interface MissionCell : UITableViewCell extern NSString * const MISSION_CELL_IDENTIFIER; @property (nonatomic, weak) IBOutlet UILabel *missionName; @property (nonatomic, weak) IBOutlet UILabel *missionID; @property (nonatomic, weak) IBOutlet UILabel *rocket; @end

Notice the definition of our constant reuse identifier string is preceded by the keyword extern, which indicates that the variable is global and will be defined elsewhere (in this case, the .m file). Using extern in the definition means that any class importing this header file will receive access to this constant. In the implementation, we need only to define the constant with a value and synthesize our variables. Nicolaas tenBroek (AMDG) 154

iOS Development MissionCell.m


#import "MissionCell.h" @implementation MissionCell NSString * const MISSION_CELL_IDENTIFIER = @"MissionCell"; @synthesize missionName; @synthesize missionID; @synthesize rocket; @end

Tables

That is all the coding we will require for our cell. Obviously we could write any additional methods we might need, but for our current purposes the minimum will suffice. Next we need to make few minor modifications to the Controller. As we mentioned earlier we will be loading new instances of our cell in a different fashion than in previous examples. In order to facilitate that process we will need to add a new IBOutlet to our header file. The changes are in bold below (though the synthesis of the property is not shown). ViewController.h
#import <UIKit/UIKit.h> #import "MissionCell.h" @interface ViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> @property (nonatomic, weak) IBOutlet UITableView *table; @property (nonatomic, weak) IBOutlet UIButton *editButton; @property (nonatomic, weak) IBOutlet MissionCell *missionCell; - (IBAction)toggleEditMode; @end

Now we can modify the tableView:cellForRowAtIndexPath: method. The reuse identifier is in the new constant we created, so we do not need to create the static variable we used before. Notice that we can directly reference the constant MISSION_CELL_IDENTIFIER in the dequeueReusableCellWithIndentifier: method call. As before, if a cell was not returned from the dequeue request, we will need to make a new one. This time however we will create a new cell by directly loading the nib file. The method loadNibNamed:owner:options: is a UIKit addition to the NSBundle class. The nib name argument is simply the name of the nib file minus the extension. Make sure it is spelled properly because the

Nicolaas tenBroek (AMDG)

155

iOS Development

Tables

compiler cannot check the spelling for us to ensure the file exists. The self argument to owner will be used in setting the IBOutlet we created earlier. Next we simply assign our local cell variable from the outlet and then set the outlet to nil (which ensures we do not accidentally reuse a non-dequeued cell later). Now that we have a cell we can set the data for display via the outlets. If you compare those lines to the ones we listed as an example at the beginning of this section, you will see that they are far easier to read. Given that we are not casting, these statements can also be type-checked by the compiler, which means fewer run-time bugs for us to deal with. ViewController.m
#pragma mark - UITableViewDataSource Methods - (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MissionCell *cell = (MissionCell *)[tableView dequeueReusableCellWithIdentifier:MISSION_CELL_IDENTIFIER]; if(!cell) { [[NSBundle mainBundle] loadNibNamed:@"MissionCell" owner:self options:nil]; cell = missionCell; missionCell = nil; } Mission *mission = [[[tableData objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row]; cell.missionName.text = mission.name; cell.missionID.text = mission.missionID; cell.rocket.text = mission.rocket; return cell; }

All that is left now is to design the cell itself and wire it up. We begin with a bit of clean-up as the prebuilt nib file contains a UIView we do not need. You can delete it by dragging it off the XCode window (watch for the icon to change to a crumpled paper, and then drop the item) or simply selecting the view and pressing the delete key. After you have disposed of the existing view, grab a Table View Cell from the object list and drag it onto the edit space.

Nicolaas tenBroek (AMDG)

156

iOS Development

Tables

Add some labels for the mission, ID, and rocket. You can modify the look of those labels to your hearts content. We have simply changed the colour and the font size for our example. Place them wherever you would like within the view.

Before we wire up the new labels we need to make some changes to allow our controllers new IBOutlet to be connected to the cell. This setup will be a bit different from the others we have used so far. Begin by selecting Files Owner and then open the Identity Inspector. Change the class to ViewController.

Next, select the UITableViewCell in the left-side menu and change its class in the Identity Inspector to MissionCell. Note: It is best to make this selection via the left-side menu so you do not accidentally select one of the labels (or other items) you have placed on the cell.

With these changes made we can now connect Files Owners missionCell outlet to the cell. Then connect the missionName, missionID, and rocket outlets from the cell itself to the labels. We cannot access those outlets from Files Owner because it is connected to the table controller, not the cell controller. So, right-click on the cell from the left side menu and make the connections there.

Nicolaas tenBroek (AMDG)

157

iOS Development

Tables

One more step to go and then our project will be complete. With the cell selected switch to the Attributes Inspector and set the identifier to our reuse identifier. This step is critical! Be very careful. The string in the Identifier field must match the string in our code exactly or we will have both memory and performance issues. In fact, its best if you copy it from your code and paste it here.

Thats it! Save and run!

With our new cell design in place our table both looks sharp and is more informative than before. We have even managed to implement nearly all the common table actions in a fairly small project.

Nicolaas tenBroek (AMDG)

158

iOS Development

Tables

UITableViewController
Back at the beginning of the chapter we promised to show you two different ways to create a table. We have already seen the first approach, which is to create a View and Controller and then implement the UITableViewDataSource and UITableViewDelegate protocols. The second approach is to extend the class UITableViewController. We have already covered a great deal of what you will need to know about tables, and all of that will be continue to be useful when using the UITableViewController. This class simply provides a convenient package for everything you need to build a table. It implements the data source and delegate protocols and uses a UITable as its view (rather than including a UIView as we did in our earlier examples). We will need to build another project for this example, but XCode does not provide a template for a straightforward table-based application. So, we will use this opportunity to start with a bare-bones template and learn how to add a view to it. Just to make things interesting we will also add a gesture recogniser to notify us when the user double-taps the screen. Start a new project from the File menu, and choose Empty Application.

We named our project AnotherTable.

Nicolaas tenBroek (AMDG)

159

iOS Development

Tables

This produces a nearly empty project with nothing but the App Delegate and a window, so our next task is to add the table. Right-click on the AnotherTable folder and choose New File.... Choose UIViewController subclass and check both the UITableViewController subclass and With XIB for user interface checkboxes. Name your new class AnotherTable.

Name your new class AnotherTable: and ensure it is a subclass of UITableViewController.

So far the loading of an apps initial view has been handled for us, and consequently we have ignored the process. Because our app was not created with a view, the code to load it was not included in our template. The class responsible for loading and displaying the initial view is the App Delegate. Before our table can be displayed we will need to inform the App Delegate about the new view and create an instance. We will begin by editing the App Delegate header where we need to create a property to hold the view. The changes needed are in bold below.

Nicolaas tenBroek (AMDG)

160

iOS Development AppDelegate.h


#import <UIKit/UIKit.h> @class AnotherTable; @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) AnotherTable *anotherTable; @end

Tables

The @class statement simply tells XCode that a class exists with the given name, but does not actually give us access to the class. This is the preferred approach in the App Delegate as many classes will need to import the App Delegate to gain access to its resources. The @class statement avoids cyclical import statements that can arise in those situations. Next we need to edit the source file so that an instance of our new view is created and set as the root when the application launches. Again the changes are in bold below. AppDelegate.m
#import "AppDelegate.h" #import "AnotherTable.h" @implementation AppDelegate @synthesize window = _window; @synthesize anotherTable; - (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.window.backgroundColor = [UIColor whiteColor]; self.anotherTable = [[AnotherTable alloc] initWithNibName:@"AnotherTable" bundle:nil]; self.window.rootViewController = self.anotherTable; [self.window makeKeyAndVisible]; return YES; } /* */ Remaining File Removed For Brevity

Nicolaas tenBroek (AMDG)

161

iOS Development

Tables

As shocking as it may be, with a few more changes we will have a completed app. At this point the only work remaining is to create a model and use it to inform the UITableViewDataSource methods. We will use an array as our model again as it is an ideal and simple fit for our purposes. We also need to add a gesture recogniser to inform us of the users taps. Each time they double-tap we will add a timestamp to the array, and then inform the table that the model has changed by calling the method reloadData. Given the extreme simplicity of our app we will not need to modify the tables header file at all. So, we begin by adding an array instance variable. When you open AnotherTable.m you will notice a huge change from the template we saw after adding a standard ViewController file. Now most of the methods for the UITableViewDataSource and UITableViewDelegate are present but commented out or partially implemented. The required methods are the only ones not commented out, which makes getting the table up and running quite a bit easier. In the interest of brevity we have removed all the commented methods and those methods that will not be changed from the listing below. Before you delete any commented methods you should examine their contents as they often contain sample code and suggested uses for the method. AnotherTable.m (Section 1 of 4)
#import "AnotherTable.h" @implementation AnotherTable { @private NSMutableArray *tableData; }

Next we need a new method to add a new row to the table. Our data will simply be the timestamp when the row was created. Using the timestamp will ensure that each row contains a unique piece of data and help us see the changes to the table more easily. AnotherTable.m (section 2 of 4)
- (void)addRow { [tableData addObject:[[NSDate date] description]]; [self.tableView reloadData]; }

Here you see our very simple addRow method that adds the timeStamp to the model and calls reloadData using a property called tableView. We did not create tableView property of course; it was inherited from the UITableViewController and serves the same purpose as the table property we created in our earlier examples.

Nicolaas tenBroek (AMDG)

162

iOS Development

Tables

Next we need to implement our data source methods. Our table will be as simple as possible with one section, and the default cell layout. AnotherTable.m (section 3 of 4)
#pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [tableData count]; } - (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } cell.textLabel.text = [tableData objectAtIndex:indexPath.row]; return cell; }

Finally we will create our data model, and also setup the double-tap gesture. To keep the code as simple as possible we pushed the creation of the array into viewDidLoad. Doing so avoided the need to override initWithCoder:. Obviously in a real app it would be worth the effort to override the init method, but we are not planning to reuse or extend this example. AnotherTable.m (section 4 of 4)
#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; tableData = [[NSMutableArray alloc] init]; UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addRow)]; doubleTap.numberOfTapsRequired = 2; [self.view addGestureRecognizer:doubleTap]; }

Nicolaas tenBroek (AMDG)

163

iOS Development

Tables

Once all those methods have been completed, you can build and run the app to see a complete working table-based app with minimal effort. Obviously we do not need to use the UITableViewController, but it does make the job quite a bit easier. The UITableViewController is used in some app templates, so it is one you will be seeing and using in the future. The important thing to remember about it is that this class is merely a convenience class. It neither adds nor subtracts additional abilities from classes you create.

Nicolaas tenBroek (AMDG)

164

iOS Development

Tables

Nicolaas tenBroek (AMDG)

165

iOS Development

In-App Navigation

In-App Navigation
Our example apps up to this point have been terribly simple affairs. They have entirely consisted of one small screen of information, and perhaps a pop-up dialogue or two. Of course, most (though certainly not all) real apps will require multiple screens. As soon as you need more than one screen, you have to start thinking about how the user will transition from one screen to the next, and how they will get back if necessary. The ability to easily and intuitively move from one screen to the next becomes critical functionality that actually forms a base for the user experience within your app. If done fairly well the user will easily be able to move about within the app after only a brief consideration. Done poorly, the user will be frustrated and unsure of how to proceed. If done perfectly the user wont have to think about the navigation at all, and will be able to navigation through your application as if they have been using it for years. Of course, that is the ultimate goal of UI design in general, but it becomes incredibly important with navigation. Remember that with mobile applications users are often in less-than-ideal environments, and will not be able to devote much attention to figuring out a complex UI. You never want to create a situation where users are left wondering where they need to go to get to a specific feature within your app, and feel the need to start poking around until they find it. Instead we want things so obvious that the user isnt consciously aware of their decision making process. When we apply this idea to navigation it means users should never feel lost within an app. They should always know where they are, how they arrived at that location, and how to proceed to the next place they want to go. This can be achieved any number of ways from swiping or tapping to go to the next page of a book, to bumping the device causing on screen objects to move, to a simple button at the top of the screen that points back in the opposite direction of the last screen, to virtually anything else you can imagine. The actual mechanism is not what is important. What is important is that the mechanism must feel a natural and organic part of the app, not something that has been tacked on. The requirement of natural feel forces navigation-based decisions to be made in the very early design stages as those decisions could easily affect the layout of all the screens within the app. This is a point in development where some time spent in serious thought can have huge payoffs later. You should be aware that the tacked on feeling can arise from at least two very different situations. One of those happens when navigation decisions are made too late in the design process. Forcing a late-timed navigational element back through the screens will almost always interfere with the design of some screens within the app. The second situation causing the tack on feeling happens when designers become enamoured with a certain mechanism because of its perceived wow factor. Those designers will jump through hoops and bend over backwards trying to make the mechanism fit their apps. As cool as it may be, if the navigation is not a natural part of your app, it will be blatantly obvious and will destroy the user experience. A plain but natural navigation is far better than an ill-fitting cool navigation. While there is no limit to the number of ways navigation can be achieved within an app, iOS has built-in support for a few common scenarios. For other scenarios you will have to build the navigation yourself. Four of the scenarios are common across iOS platforms, and one is unique to the iPad as it is designed to use the additional screen space afforded with the larger device. One of the common scenarios involves

Nicolaas tenBroek (AMDG)

166

iOS Development

In-App Navigation

a bar across the top of the app and new screens sliding in from the side (typically flowing from right to left). The bar at the top contains a back button pointing to the left, and tapping it causes the current screen to slide back off in the opposite direction. This scenario was rather uncreatively called a Navigation-based Application in the app templates and enforces a linear or tree-based style of navigation. Users are forced to follow a path to get from one place to another. If they want to change paths, they must backtrack to the point at which the paths diverged and choose another. The Navigation-based Application template was inexplicably removed in XCode 4.2, though it is still a supported and recommended approach. Now you are simply forced to build the app from scratch which can be very easy if using a storyboard, or slightly more complicated if not. Another common scenario involves the concept of tabs. A bar is presented along one edge of the screen displaying a set of tabs. When the user presses a tab, the display changes to show the single screen associated with that tab. This navigation style is more random than the path style because users can choose at any time to move directly to any other section. There is no need to backtrack, and each navigation decision has no discernible effect on the next one. The tab bar approach has some drastic limitations on application size, as there is a severely limited space available for tabs. The actual amount of space depends on the size of screen edge where the bar is placed. The third common scenario is a simple two-screen setup. Users press a button and the second screen animates into place, typically by flipping over as if to display the back of the screen they are currently viewing. Pressing a done button reverses the animation to show the front of the screen again. This setup is most often used for applications that have a single main screen and need an additional screen for preferences or settings. The template for kind of application is called a Utility Application. The last common scenario was introduced in iOS 5 and is called a Page-Based Application and acts much like an e-book. The typical page-based application uses a single view presented multiple times with different data for each presentation. Page-based applications can display either one or two pages at a time and users typically tap one side of the screen to navigate to the next page and the opposite side to navigate to the previous page. The navigation scenario that is unique to the iPad is a derivative of the tab bar approach, but the tab bar is replaced with a table. The table can be constantly displayed, splitting the screen between the table and a content area, or collapsed under a button and displayed over the top of the content in a popup when the user wants to go somewhere else. This scenario offers a tabbed approach to navigation without the severe screen limits of the tab bar. The additional screen spaced offered by the iPad means that tables could be much longer and still easily navigable. The table of contents for a book might be a good analogy for this approach. In iOS 5 Apple introduced the Storyboard concept for apps with multiple views. The storyboard collects all the views for an app into a single file (a .storyboard file) and graphically represents the flow from screen to screen. The storyboard offers some advantages over the individual nib approach, but at a cost. The largest cost comes in the form of device support. Storyboard-based apps are restricted to devices running at least iOS 5.0, which at the time of this writing eliminates millions of devices that

Nicolaas tenBroek (AMDG)

167

iOS Development

In-App Navigation

cannot be upgraded. Obviously over time this number will decrease, but it is an issue you must consider when designing an app (devices lost are customers lost). The advantages of using the storyboard approach are mostly easing the burden of development, though this will likely change as the concept is developed further. Currently storyboards help by collecting all your views into one place, giving an overview of flow through an app, and automating some (though not necessarily all) of the process of transitioning from one view to another which reduces the amount of code to write. To demonstrate both the uses of the storyboard and to compare the coding differences, we will develop each example in this chapter two times: once with the storyboard and once without it.

Nicolaas tenBroek (AMDG)

168

iOS Development

In-App Navigation

Utility Application
As we mentioned earlier, the Utility Application is best used for applications that have two screens: one main screen and a second, supporting screen. The second screen is typically used for preferences or settings that control how the main screen behaves. The template for this style of app includes an info style button on the main screen and a done style button on the second screen. Pressing either button will cause the screen to flip over (rotate on the vertical axis), displaying the other screen. You are not required to use these of course, as the actions can be initiated from most any control or even a gesture. Keep in mind though that whatever you choose must be obvious to the user, and users already know what the info button does. We will demonstrate this approach by creating a very basic flashlight app. The main screen will be our flashlight, while the second screen will offer some customisation options. In XCode, create a new project, and select the Utility Application template.

Name the application Flashlight, select to use ARC, but not to use the Storyboard, and save it. Before we write any code we can run the app and get a sense of how it will look. Right away we are presented with a grey screen and a white info button.

Nicolaas tenBroek (AMDG)

169

iOS Development

In-App Navigation

Pressing the button animates the main screen to reveal the back. This second screen contains a toolbar across the top with a blue done button and a screen title in white. Pressing Done will animate the screen again, reversing the display. Thats a pretty good start considering we havent actually done any work.

Nicolaas tenBroek (AMDG)

170

iOS Development

In-App Navigation

Before we begin to change things, lets take a look at the code we received from the template. There is quite a bit going on in the code, so it is worth taking a few minutes to explore. The Utility Application template generates two view controllers, one named MainViewController and one named FlipsideViewController. MainViewController.h
#import "FlipsideViewController.h" @interface MainViewController : UIViewController <FlipsideViewControllerDelegate> - (IBAction)showInfo:(id)sender; @end

Notice that the MainViewController implements a protocol called FlipsideViewControllerDelegate. This protocol is actually defined within the FlipsideViewController and defines the method flipsideViewControllerDidFinish:. This method serves to inform the main view when the flip side view has completed its work and is the appropriate place to look for changes in the settings or preferences. We also have an IBAction method that will be called when the user presses the info button. This action method will cause the flip side view controller to be displayed. Now we can take a look at the implementation. The template provides several helpful methods that are commented out or with empty implementations, but we have removed those from the current listing simply in the interest of brevity. You should consider keeping those extra methods for now as you may need them later. They are easy enough to delete when you have finished the app if you find you do not need them. MainViewController.m
#import "MainViewController.h" @implementation MainViewController #pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller { [self dismissModalViewControllerAnimated:YES]; } - (IBAction)showInfo:(id)sender { FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:@"FlipsideViewController" bundle:nil]; controller.delegate = self; controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal; [self presentModalViewController:controller animated:YES]; }

Nicolaas tenBroek (AMDG)

171

iOS Development MainViewController.m


@end

In-App Navigation

The showInfo: method begins by creating an instance of the FlipsideViewController. Each time the info button is pressed we see a new copy of the controller. While this may not seem terribly efficient, remember that this view is not meant to be active for much of the applications lifespan. It is something we will need only occasionally and can be discarded when not in use to save system resources. After creating the controller we set the main view as the delegate, and then setup the animation we want to use. The default template uses a horizontal flip, but you can also choose to have the new view slide up from the bottom, be revealed through a dissolve, or have the main view curl up to reveal the flip side view. Next the method presentModalViewController:animated: is called. A modal view in iOS is considered a temporary view that is displayed on top of the current view (hierarchically) and will begin receiving input. The temporary view will continue to receive input until it is dismissed through the method dismisModalViewControllerAnimated:. While a modal view is on the screen, the original view is prevented from working. In the delegate protocol method flipsideViewControllerDidFinish: you can see that we call dismissModalViewControllerAnimated: which will restore the main view to the screen. This setup may seem a bit funny, and in fact, if this were all the code we needed, you would be correct in that assessment. Keep in mind though, that the flipside view is usually used to store settings and preferences. So, the flipsideViewControllerDidFinish: method is the place where the main view can react to the changes in those settings. That is all there is to the main view, so now we shall examine the flipside view. Again we will start in the header file. This class is only slightly more complex than the main view. Here you can see that the FlipsideViewControllerDelegate protocol is defined, a delegate property is created, and an action method is defined for handling the done button press. FlipsideViewController.h
#import <UIKit/UIKit.h> @class FlipsideViewController; @protocol FlipsideViewControllerDelegate - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; @end @interface FlipsideViewController : UIViewController @property (weak, nonatomic) IBOutlet id <FlipsideViewControllerDelegate> delegate;

Nicolaas tenBroek (AMDG)

172

iOS Development FlipsideViewController.h


- (IBAction)done:(id)sender; @end

In-App Navigation

Now lets look into the implementation. Despite the more complex header file, the implementation of the FlipsideViewController is substantially simpler than the MainViewController. Once again, we removed the empty template methods to help us focus on the important parts. FlipsideViewController.m
#import "FlipsideViewController.h" @implementation FlipsideViewController @synthesize delegate = _delegate; #pragma mark - Actions - (IBAction)done:(id)sender { [self.delegate flipsideViewControllerDidFinish:self]; } @end

Here we see the done method simply tells the delegate that it is time to redisplay. Everything else is standard. All in all, this view controller is pretty basic and its code is quite a bit less complicated than the main view controller. Now that we have an understanding of the basic setup and functioning, we can start adding the necessary controls and code for our flashlight. We will not be adding controls to the main view as it is our light. The only thing we want obscuring the flashlight is the info button, which means nearly all the changes to the main view will all be in code. On the flipside view we will be giving the user the ability to set the colour of the flashlight. Unfortunately iOS does not have a built-in colour choosing control. While you can find plenty of sample code for making a full-fledged colour chooser on the Internet, that kind of power is a bit beyond what we will need for this example. So, with that in mind, we need a way to display a colour sample to the user and some way for the user to specify colours. We could make a list of built-in colours and display those, but that would limit the users choice and might actually take more code to implement than other methods. Instead we will allow the user to set their colour by mixing the red, green, and blue values. (Note: this would be just as easy to do with hue, saturation, and brightness). A label will work well for the colour swatch, and sliders will be easy controls for mixing the colours. We will need to make properties for all the controls, and an action to respond to the sliders.

Nicolaas tenBroek (AMDG)

173

iOS Development FlipsideViewController.h


#import <UIKit/UIKit.h> @class FlipsideViewController; @protocol FlipsideViewControllerDelegate - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; @end @interface FlipsideViewController : UIViewController @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet id <FlipsideViewControllerDelegate> delegate; UISlider *redSlider; UISlider *greenSlider; UISlider *blueSlider; UILabel *colourSample;

In-App Navigation

- (IBAction)done:(id)sender; - (IBAction)sliderValueChanged; @end

You may have noticed that we only added one action method even though we have three sliders. The sliders will each call the same action method because we need to do the same action (display the new colour) regardless of the slider being modified. Now we can move to the implementation file. FlipsideViewController.m
#import "FlipsideViewController.h" @implementation FlipsideViewController @synthesize @synthesize @synthesize @synthesize @synthesize delegate = _delegate; redSlider; greenSlider; blueSlider; colourSample;

#pragma mark - Actions - (IBAction)done:(id)sender { [self.delegate flipsideViewControllerDidFinish:self]; } - (IBAction)sliderValueChanged { self.colourSample.backgroundColor = [UIColor colorWithRed:self.redSlider.value green:self.greenSlider.value blue:self.blueSlider.value alpha:1.0]; } @end

Nicolaas tenBroek (AMDG)

174

iOS Development

In-App Navigation

With the exception of handling our properties, we added only one line of code. In the sliderValueChanged method we set the background colour of our colour swatch label by creating a UIColor object using values from the sliders. UIColor requires its data to be double values ranging from 0.0 to 1.0. This is the default value range for a UISlider, so that makes our job even easier. Now lets add those controls to our view and wire them up.

Please note: to wire up the sliderValueChanged action, you will need to right click on each slider and connect Value Changed from the slider to the sliderValueChanged method of Files Owner. Be sure to do this for each slider. (For the purposes of creating this screen image the labels background colour was changed to white.) Next, we need to modify our main view controller so that it can pick up this new data. We will not need additional controls for the main view, so we can move right to the implementation. MainViewController.m (section 1 of 3)
#import "MainViewController.h" @implementation MainViewController #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; //start with white light self.view.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0]; }

Nicolaas tenBroek (AMDG)

175

iOS Development

In-App Navigation

Notice that in viewDidLoad we set the background colour of the main view to bright white. This way our flashlight will always start by displaying a bright white screen. In a later chapter we will learn to store the users preferences and settings. With that knowledge we could change this code to load the last colour selection the user made, which would be much more appropriate. For now though, we will have to be satisfied with always starting with the default setup. In the next section we see flipsideViewControllerDidFinish:. Remember that this method is called when the user presses the done button. After dismissing the flipside view we pick up the users colour choice from the swatch, and use it to set the main views background colour. MainViewController.m (section 2 of 3)
#pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller { [self dismissModalViewControllerAnimated:YES]; self.view.backgroundColor = controller.colourSample.backgroundColor; }

The last change we need to make is in showInfo:. Here we get the main views background and use it to set the current values of the sliders and swatch. That way the user will have a meaningful starting point on the settings screen. As an added bonus, setting the sliders to the current colour means we will not need extra code to figure out if the user hit the done button without making changes to the sliders. MainViewController.m (section 3 of 3)
- (IBAction)showInfo:(id)sender { FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:@"FlipsideViewController" bundle:nil]; controller.delegate = self; controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal; [self presentModalViewController:controller animated:YES]; const CGFloat *colourComponents = CGColorGetComponents(self.view.backgroundColor.CGColor); controller.redSlider.value = colourComponents[0]; controller.greenSlider.value = colourComponents[1]; controller.blueSlider.value = colourComponents[2]; controller.colourSample.backgroundColor = self.view.backgroundColor; }

Nicolaas tenBroek (AMDG)

176

iOS Development

In-App Navigation

We have one last modification before running. The info button on the main view is white. We have changed the background of the main view to white as well, which will make that button all but invisible. Fortunately there is a dark grey version of the info button available. So, open MainViewController.xib in Interface Builder, select the info button, and in the Attributes Inspector change the button type to Info Dark.

Obviously this is not a perfect solution, as the button will disappear again if the user picks a dark colour for their flashlight, but it will be better. If you want to go really crazy with it, you can use the colour selection to decide what style of button to use and change it dynamically. That kind of activity is a bit more than we need for our sample though, so we will leave that as an exercise for the reader. With those changes in place our flashlight is complete! Save, build, and run and we have a pretty decent flashlight app with user customisable colours. We cannot directly control the brightness of the screen from within our app, but we could simulate brightness change by adding another slider to control the alpha values. That would be a very easy modification should you wish to make it. Currently the API does not support changes to the screens brightness, so all apps control brightness by changing the background colour or transparency.

Nicolaas tenBroek (AMDG)

177

iOS Development

In-App Navigation

Utility Application Using Storyboard


Now that we have a working Utility Application, we can recreate it using the Storyboard. Create a new project, again choose the Utility Application template, but this time ensure that both ARC and Storyboard are selected. Taking a look at the files available, you will notice that they are nearly identical to what we had in the previous project. In fact, the only obvious difference is that the two nib files have been replaced with a file called MainStoryboard.storyboard. Click on that file and it will open in Interface Builder. You should see a screen like this:

The setup in the Storyboard is quite a bit different from the nib; so lets take a moment to look around. On the left side of the IB Window we see the listing for each of the views contained in the Storyboard (which is two for this example). In the middle of the window is the standard view designer and as you might expect both views are present, but with some additional elements. Each view has a bar below the view listing the name of the view. While in editing mode, this bar will also provide access to the controller associated with the view. When editing a large Storyboard it will be far more convenient to access the controller through this bar than it will be to navigate a long list on the left side of the screen. The other items that you should notice are the grey arrows connecting the screens. The Main View Controller has an arrow that does not have an origin. This arrow indicates the starting point for the

Nicolaas tenBroek (AMDG)

178

iOS Development

In-App Navigation

application. The other arrow joins the two screens to indicate that an action on the Main View Controller will cause the Flipside View Controller to be displayed. If you click on the connection the corresponding control will highlight on the View, which at the moment is the only way to know which control caused the connection. We will not go into how to create those connections at the moment, though we will cover that soon. Finally, the lower-right side of the screen contains three new buttons: zoom-out; zoom-to-100%; and zoom-in. These buttons will prove helpful in navigating the Storyboard when you begin dealing with more than a few views. Frankly, an extra-large monitor would be a great help as well and might even be a necessity for complex projects. The changes caused by using a Storyboard are not limited to the nib files. If we look into MainViewController.h we find a nearly empty file. MainViewController.h
#import "FlipsideViewController.h" @interface MainViewController : UIViewController <FlipsideViewControllerDelegate> @end

Notice the lack of IBAction method here. Without a Storyboard we needed an IBAction to process the button press, but now that activity is being handled by the Storyboard. If we look into the controllers source file we see additional changes. (In the listing below we have eliminated all empty and commented methods.) MainViewController.m
#import "MainViewController.h" @implementation MainViewController #pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller{ [self dismissModalViewControllerAnimated:YES]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"showAlternate"]) { [[segue destinationViewController] setDelegate:self]; } } @end

Nicolaas tenBroek (AMDG)

179

iOS Development

In-App Navigation

The delegate method flipsideControllerDidFinish: is exactly the same as it was in the previous example, but the IBAction has been replaced with prepareForSegue:sender:. The Storyboard will call this method before transitioning to a new view, which provides an opportunity to send some data to the new view before it is displayed. You can see here that the template setup the delegate much as the IBAction method did in the previous example. The passed in UIStoryboardSegue object gives us access to the source and destination controllers and the identifier for the connection. Each connection made in the Storyboard has an identifier property that is simply a unique string (typically set in IB). The prepareForSegue:sender: method is both a strength and weakness of the Storyboard approach. If the current view does not need to send any information to the new view, then this method does not need to be implemented, which results in less code (and less code is always good). We would consider that situation a win for the programmer. However, if data needs to be sent to the new view we would need to examine the situation further. In that case, if the current view only connects to one other view, then the implementation of this method is essentially the same as the IBAction method because the if statement to check the identifier as presented in this template is really not needed. In fact, this app would run exactly the same way with or without that check. If we eliminate useless code (again, always a good idea), then the amount of code to write the IBAction and prepareForSeugue:sender: is essentially the same. We would consider this a tie for the programmer. If the current view connected to multiple other views and needed to send data, then you are stuck coding multiple if statements much like the one shown in the example. This effectively takes our code back to the relative stone age of Structured Programming, eliminating the benefits provided by each object calling a unique method (the OOP approach). This situation produces an obvious loss for the programmer both in terms of code complexity and fragility (note that the checks are dependent on hard-coded strings that have to be replicated exactly in the source file AND Interface Builder). Obviously the Storyboard is not a programming panacea fixing all the troubles inherent in apps with multiple views. Nonetheless, this is the first version of this approach, and it will hopefully be refined as time goes on. Meanwhile, we will forge ahead with learning to use Storyboards and admonish the reader to use them only where they are appropriate. Much like we did in the previous example, we need some simple changes in the source file for the MainViewController. It may already be obvious, but there are two ways to write the code in the prepareForSegue:sender: method. We will present both approaches and the reader can choose the one they like best. MainViewController.m (version 1)
#import "MainViewController.h" @implementation MainViewController //Empty Methods Removed #pragma mark - View lifecycle

Nicolaas tenBroek (AMDG)

180

iOS Development MainViewController.m (version 1)

In-App Navigation

- (void)viewDidLoad { [super viewDidLoad]; //start with white light self.view.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0]; } #pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller{ [self dismissModalViewControllerAnimated:YES]; self.view.backgroundColor = controller.colourSample.backgroundColor; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"showAlternate"]) { [[segue destinationViewController] setDelegate:self]; const CGFloat *colourComponents = CGColorGetComponents(self.view.backgroundColor.CGColor); [[segue destinationViewController] redSlider].value = colourComponents[0]; [[segue destinationViewController] greenSlider].value = colourComponents[1]; [[segue destinationViewController] blueSlider].value = colourComponents[2]; [[segue destinationViewController] colourSample].backgroundColor = self.view.backgroundColor; } @end }

The code necessary for viewDidLoad and flipsideViewControllerDidFinish: is exactly the same as what we used in the previous example. The prepareForSegue:sender: gives us access to the FlipsideViewController, but the data type is the base class ViewController. That means we cannot directly access the properties, and need to access them through their methods. The next version of this file shows a different approach to writing this code. MainViewController.m (version 2)
#import "MainViewController.h" @implementation MainViewController //Empty Rmethods Removed #pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller{ [self dismissModalViewControllerAnimated:YES]; }

Nicolaas tenBroek (AMDG)

181

iOS Development MainViewController.m (version 2)


- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"showAlternate"]) { FlipsideViewController *flipsideController = [segue destinationViewController]; flipsideController.delegate = self;

In-App Navigation

} } @end

const CGFloat *colourComponents = CGColorGetComponents(self.view.backgroundColor.CGColor); flipsideController.redSlider.value = colourComponents[0]; flipsideController.greenSlider.value = colourComponents[1]; flipsideController.blueSlider.value = colourComponents[2]; flipsideController.colourSample.backgroundColor = self.view.backgroundColor;

In this version we created a variable of type FlipsideViewController, and used that variable for direct access to the properties. The end result is exactly the same, so you can use whichever approach appeals to you. With the MainViewController complete we can turn our attention to the FlipsideViewController. We shall set it up in exactly the same way as we did in the previous example. FlipsideViewController.h
#import <UIKit/UIKit.h> @class FlipsideViewController; @protocol FlipsideViewControllerDelegate - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; @end @interface FlipsideViewController : UIViewController @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet id <FlipsideViewControllerDelegate> delegate; UISlider *redSlider; UISlider *greenSlider; UISlider *blueSlider; UILabel *colourSample;

- (IBAction)done:(id)sender; - (IBAction)sliderValueChanged; @end

The source file is also exactly the same as it was in the previous example.

Nicolaas tenBroek (AMDG)

182

iOS Development FlipsideViewController.m


#import "FlipsideViewController.h" @implementation FlipsideViewController @synthesize @synthesize @synthesize @synthesize @synthesize delegate = _delegate; redSlider; greenSlider; blueSlider; colourSample;

In-App Navigation

#pragma mark - Actions - (IBAction)done:(id)sender { [self.delegate flipsideViewControllerDidFinish:self]; } - (IBAction)sliderValueChanged { self.colourSample.backgroundColor = [UIColor colorWithRed:self.redSlider.value green:self.greenSlider.value blue:self.blueSlider.value alpha:1.0]; } @end

The next step, as always, is to wire-up the controls. Click on the Flipsides View in the Storyboard and the black bar below the View should change to display the First Responder and the Flipside View Controller. If it does not, try zooming in. The initial implementation of the Storyboard only allows editing a View if it is zoomed in to a fair degree. Your screen should look something like the screen shot below.

Nicolaas tenBroek (AMDG)

183

iOS Development Now, right-click on the View Controller to bring up the context menu:

In-App Navigation

As you can see, the context menu is a bit larger when using the Storyboard. We now have entries managing segues from this screen as well as referencing segues which come to this screen. We do not need those extra entries in this example, but we do need to wire-up the controls and actions. Do that now. Now run the app and change the colour of the flashlight a few times. When you display the controls view you likely noticed that the sliders did not change their positions to match the colour, nor did the colour sample change. What is happening here is quite interesting. In the previous (non-storyboard) version of this app we set the values in the controls after the view had been presented. In this version we set those values in the prepareForSegue:sender: method, which is called before the view is presented. If you recall from a few chapters back, the controls are not actually created and accessible until after viewDidLoad runs. This puts us in a bit of a bind, because the method designed to allow us to pass information to the new view is being called before the controls are accessible. In fact, if you walk through this code in the debugger you will find that the references are all nil. Fortunately the fix is easy enough, and frankly better follows property Object-Oriented Design rules. The solution is to add a colour property to the Flipside controller and use that property to pass data back and forth. It then becomes the Flipside controllers responsibility to setup its own controls, which while more complicated, also shields our other code from any future modifications. If later you decided to replace the sliders with a proper colour picker, you would be able to do that without changing the main controller in any way. We will begin in the Flipsides header file, where we need only to add a property for the colour. That line is in bold below:

Nicolaas tenBroek (AMDG)

184

iOS Development FlipsideViewController.h (fixed version)


#import <UIKit/UIKit.h> @class FlipsideViewController; @protocol FlipsideViewControllerDelegate - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; @end @interface FlipsideViewController : UIViewController @property (weak, nonatomic) IBOutlet id <FlipsideViewControllerDelegate> delegate; @property (nonatomic, weak) IBOutlet UISlider *redSlider; @property (nonatomic, weak) IBOutlet UISlider *greenSlider; @property (nonatomic, weak) IBOutlet UISlider *blueSlider; @property (nonatomic, weak) IBOutlet UILabel *colourSample; @property (nonatomic, strong) UIColor *flashlightColour; - (IBAction)done:(id)sender; - (IBAction)sliderValueChanged; @end

In-App Navigation

The source file will require a few more changes than the header did, but they too will be quite small. Again the modifications are listed in bold below. FlipsideViewController.m (fixed version)
#import "FlipsideViewController.h" @implementation FlipsideViewController @synthesize delegate = _delegate; @synthesize redSlider; @synthesize greenSlider; @synthesize blueSlider; @synthesize colourSample; @synthesize flashlightColour; #pragma mark - View lifecycle - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; const CGFloat *colourComponents = CGColorGetComponents(self.flashlightColour.CGColor); self.redSlider.value = colourComponents[0]; self.greenSlider.value = colourComponents[1]; self.blueSlider.value = colourComponents[2]; self.colourSample.backgroundColor = self.flashlightColour; }

Nicolaas tenBroek (AMDG)

185

iOS Development FlipsideViewController.m (fixed version)


#pragma mark - Actions - (IBAction)done:(id)sender { [self.delegate flipsideViewControllerDidFinish:self]; }

In-App Navigation

- (IBAction)sliderValueChanged { self.colourSample.backgroundColor = [UIColor colorWithRed:self.redSlider.value green:self.greenSlider.value blue:self.blueSlider.value alpha:1.0]; self.flashlightColour = self.colourSample.backgroundColor; } @end

Notice that the code in viewWillAppear: is identical to the code we had been using in the main controller. Thus, we will need to modify that file as well. The new code is in bold below. MainViewController.m (fixed version)
#import "MainViewController.h" @implementation MainViewController #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; //start with white light self.view.backgroundColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0]; } #pragma mark - Flipside View - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller{ [self dismissModalViewControllerAnimated:YES]; self.view.backgroundColor = controller.flashlightColour; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"showAlternate"]) { [[segue destinationViewController] setDelegate:self]; [[segue destinationViewController] setFlashlightColour:self.view.backgroundColor]; } } @end

Nicolaas tenBroek (AMDG)

186

iOS Development

In-App Navigation

With these modifications in place your app should run perfectly. We now have two completely different approaches to the app, and while the Storyboard may seem like overkill at this point, keep in mind that it was designed to make coding larger apps easier by drawing together the disparate parts of the app into a fairly clean interface. In addition, it forced us to change our design to better follow the precepts of Object-Oriented Programming, so that in itself is a positive step. As we continue our examination of multi-viewed apps, we will have more opportunities to compare the Storyboard with the traditional approach.

Nicolaas tenBroek (AMDG)

187

iOS Development

In-App Navigation

Tab Bar Application


The Tab Bar provides an interesting navigational aid that is also incredibly well designed. It is intuitive, easy, and fast to use. As if that were not enough good news, it is even easy to program. The tab bar allows both text and graphical features and can alert users when something needs attention. That combination of features makes it a device that programmers often want to use. Unfortunately the tab bar is not a general-purpose navigational system and should only be used if your app (or a portion of your app) fits one single scenario. The scenario where a tab bar becomes useful is when you have a group of screens that are related by general function, but are independent from one another, and each screen carries out a single dedicated task. The iPhones phone app is a perfect example of this. The phone app has a screen for favourites or quick-dial numbers, a screen showing the call log, another for accessing contacts, another that provides the number pad for dialling numbers that are not stored on the phone, and one more for accessing voice mail. Those screens are all related in that they each have something to do with phone-based communication, but are otherwise independent of one another. You can access the voicemail without going through the number pad for instance. The screens can be accessed in any order because they do not depend on one another, and they each have one discrete purpose. The heart of the tab bar system is the Tab Bar Item. The Tab Bar Item is a fairly sophisticated control with four distinct parts. Three of those parts are always visible in the app regardless of the screen being displayed. Those are the title, image, and badge. The title is perhaps obvious. It is a string that should be used to tell the user the name of the screen they will see if they tap on the item. The image is a graphic representation of the screens purpose. The image is given more prominence than the title, and is something that should be carefully considered. Images should be instantly recognisable and easily distinguishable from one another. Users will be relying on the images to help them quickly find the tab they are looking for, and images that are confusing or too similar to one another will hinder this process. The badge is an interesting item as it can be turned on or off as needed and is used to call the users attention to a particular tab. The badge is a red oval containing white text that is placed on the top right corner of the image. Badges are used in many places throughout iOS and are therefore features users will both understand and expect. For instance, any application can apply a badge to its home screen icon to inform the user of items needing attention. For instance, the e-mail app displays the number of unread messages. The calendar app uses a badge to display the number of unopened appointments. The phone app uses badges to display the number of missed calls and voicemails. The badge provides a powerful notification system that is both easy to use and instantly understandable to users. The other part of the Tab Bar Item is the view. Each Tab Bar Item contains a single view property, which is of course, the screen which will be displayed when the user taps the tab. The view can be any kind of screen, and should be a complete entity unto itself. In fact, each view should be designed so that it has only limited knowledge that it will be used on a tab. The one thing you need to consider when designing a screen to be used with a tab is the space taken up by the tab bar itself. As the tab bar is always visible you must make sure your screen does not attempt to display controls in the space occupied by the tab bar.

Nicolaas tenBroek (AMDG)

188

iOS Development

In-App Navigation

Now that we have discussed the parts of a tab bar, lets take a look at some samples. These screen shots are from the phone app, and clearly demonstrate the use of a tab bar. All of the Tab Bar Items features are clearly evident in this app. Each item has both text and graphics, and two of them use badges to alert the user. We have included three of the phone apps screens to demonstrate that the screen designs can be completely different from one another and should be purpose-built. We missed a call before taking these screen shots, so the call log item displays a badge indicating 1 missed call. Here is the number pad screen:

And now the call log screen:

Nicolaas tenBroek (AMDG)

189

iOS Development And finally, the voicemail screen:

In-App Navigation

Notice that in the last screen shot the call logs badge has disappeared. That is because we viewed that screen already and no longer need to know that calls were missed. In order for badges to be useful, they must disappear when they are no longer needed. If the badge remained and showed a 0 for instance, we would very quickly learn to ignore the badge, and it would lose its usefulness. Now that we have discussed the parts and function of a tab bar, we can build a sample app to demonstrate its use. Tab bars do not need much in the way of coding, so this will be a very short example. We will create an app with three screens. Each screen will display one search engines web page, which will allow us to compare search results side-by-side. Begin by creating an app using the Tabbed Application template in XCode. Use ARC, but not the Storyboard for this example.

The template creates an app containing two pre-built views. It names those views First and Second, which you will most likely want to change in real apps, but will suffice for our example. Open FirstViewController.h and add an IBOutlet for the web view.

Nicolaas tenBroek (AMDG)

190

iOS Development

In-App Navigation

FirstViewController.h
#import <UIKit/UIKit.h> @interface FirstViewController : UIViewController @property (nonatomic, weak) IBOutlet UIWebView *webView; @end

Next, open the .m file and add the code as listed below. You can see that we have synthesized the outlet and pre-loaded the web view with a search engines home page. In addition we replaced the strings First with Google in initWithNibName:bundle:. FirstViewController.m
#import "FirstViewController.h" @implementation FirstViewController @synthesize webView; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { self.title = NSLocalizedString(@"Google", @"Google"); self.tabBarItem.image = [UIImage imageNamed:@"first"]; } return self; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [webView loadRequest:[NSURLRequest requestWithURL: [NSURL URLWithString:@"http://www.google.com"]]]; } @end

Now edit SecondViewController.h and .m. They will need the exact same changes as you added in FirstViewController. Because the classes are separate, you can even use the same property name. Remember that the screens are independent objects from independent classes, so we do not need to worry about property names clashing, and considering our design, using the same name will actually be less confusing. (If this were a real app we would instantly recognise this as an opportunity to refactor

Nicolaas tenBroek (AMDG)

191

iOS Development

In-App Navigation

the code, eliminating the duplication.) Use www.yahoo.com as the address for the second view controllers web view. Now we need to add a third view controller. Create a standard UIViewController subclass, and make sure to check the With XIB for user interface option and select that it is a subclass of UIViewController. Name the new item ThirdViewController. The third view controller will be almost exactly the same as the first two, but use www.dogpile.com for its address. Unfortunately, we do not have an image for the third controller, but our example will work without it, so the initWithNibName:bundle: method will be a bit shorter than it was in the first two controllers. ThirdViewController.m
#import "ThirdViewController.h" @implementation ThirdViewController @synthesize webView; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { self.title = NSLocalizedString(@"Dogpile", @"Dogpile"); } return self; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [webView loadRequest:[NSURLRequest requestWithURL: [NSURL URLWithString:@"http://www.dogpile.com"]]]; } @end

Next we need to make some small modifications to the App Delegate so that it loads our new view. The changes are in bold below (as always, we have eliminated empty or unchanged methods from the listing for brevity). AppDelegate.m
#import "AppDelegate.h" #import "FirstViewController.h" #import "SecondViewController.h" #import "ThirdViewController.h" @implementation AppDelegate

Nicolaas tenBroek (AMDG)

192

iOS Development AppDelegate.m


@synthesize window = _window; @synthesize tabBarController = _tabBarController;

In-App Navigation

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. UIViewController *viewController1 = [[FirstViewController alloc] initWithNibName:@"FirstViewController" bundle:nil]; UIViewController *viewController2 = [[SecondViewController alloc] initWithNibName:@"SecondViewController" bundle:nil]; UIViewController *viewController3 = [[ThirdViewController alloc] initWithNibName:@"ThirdViewController" bundle:nil]; self.tabBarController = [[UITabBarController alloc] init]; self.tabBarController.viewControllers = [NSArray arrayWithObjects:viewController1, viewController2, viewController3, nil]; self.window.rootViewController = self.tabBarController; [self.window makeKeyAndVisible]; return YES; } @end

Now that the coding is complete (yes that was all of it!), we can direct our attention to the nib files. We will begin with FirsViewController.xib. As delivered, the view includes some helpful labels describing how to use it, and a simulated tab bar on the bottom. The simulated metrics are available on the Attributes Inspector and serve only to assist us in laying out the components and cannot be manipulated. The simulated items will be replaces with real components at run time.

Nicolaas tenBroek (AMDG)

193

iOS Development

In-App Navigation

We do not need those labels, so delete them. With our view cleaned of the unneeded items we can add the UIWebView. Select a Web View from the object list

And drag it to the view. Notice that the component resizes itself automatically to fit in the space left by the simulated tab bar.

Open the Attributes Inspector for the web view, and choose Scales Page To Fit. You can also enable the detection options if you wish.

Finally, wire-up the web view to Files Owners webView outlet. Thats everything we need to do for the first view, and the same steps need to be repeated for the second view. Given that we created it from scratch, the third view will be devoid of the helpful labels, but that means it also does not have the simulated tab bar. We need to turn that on before adding the web view so that it can size itself appropriately. To enable any simulated metrics you can select the view and then open the Attributes Inspector. We need only one, so select Tab Bar from the Bottom Bar drop down.

Nicolaas tenBroek (AMDG)

194

iOS Development

In-App Navigation

Then add a web view and wire it up. We are done. Save, build, and run! The app should start right up, though it may take a couple of seconds to display the web pages, depending on your web connection speed. Now you can quickly switch from one search engine to the next, and the pages are completely independent from one another.

Nicolaas tenBroek (AMDG)

195

iOS Development

In-App Navigation

Tab Bar Application Using Storyboard


Recreating the tab bar application using the Storyboard will be slightly more complicated than the Utility Application was, but also offers us the opportunity to see a bit more of how the Storyboard works. Start by creating a new project using the Tabbed Application template, ensuring that both ARC and the Storyboard are selected. Before we get to the storyboard, we can setup the necessary code, and we shall begin by adding a new class. Just as we did in the previous example, we need to add a new file called ThirdViewController. Ensure that the new file is a subclass of UIViewController and that the option With XIB for user interface is NOT selected. Nibs created using the new file process will not be part of the Storyboard, and while you may need to create a separate nib at some point, we do not need one at the moment. If you accidentally create an extra nib, simply delete it from the project by right-clicking the file, choosing Delete from the context menu, and then press the Delete button in the confirmation dialogue. All of the code necessary for this project will occur in FirstViewController, SecondViewController, and ThirdViewController. No changes will be required in the AppDelegate as the work it performed in the previous example has been shifted to the Storyboard. Frankly, given that our views are so simple, very little code will be required at all. The setup for the TabBarItems will be handled in the storyboard, which leaves only loading the webpage to handle in the controllers. While the setups for the FirstViewController are shown below, the second and third require similar changes, so their code is not displayed. FirstViewController.h
#import <UIKit/UIKit.h> @interface FirstViewController : UIViewController @property (nonatomic, weak) IBOutlet UIWebView *webView; @end

FirstViewController.m
#import "FirstViewController.h" @implementation FirstViewController @synthesize webView; #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [webView loadRequest:[NSURLRequest requestWithURL: [NSURL URLWithString:@"http://www.google.com"]]]; }

Nicolaas tenBroek (AMDG)

196

iOS Development FirstViewController.m


@end

In-App Navigation

The first and second views in the Storyboard are setup in a fashion similar to the way they were in the previous example:

We will need to delete the labels, add WebViews, and wire them up. In addition, we will need to change the labels displayed on the TabBarItems. In the previous example the display was handled through code, you should recall that we set the names and icons in the initWithNibName:bundle: methods. While we could certainly have setup our previous app to allow those items to be modified through the IB, it would have required far more work than it saved in coding. With the Storyboard the setup work has been done for us, so changing the attributes in IB will be a trivial task. Begin by selecting the TabBarItem labelled First:

Open the Attributes Inspector and change the Title property to Google: Nicolaas tenBroek (AMDG) 197

iOS Development

In-App Navigation

Make a similar change on the Second item, and we will be ready to add our third controller. This process will be a bit different from what we have done in the past. Select a ViewController from the object list and drag it onto the Storyboard:

You can drag any of the views to a new position in order to make it easier to both edit and to see the relationship to the other views. While you can reposition the views after zooming out, remember that you must be zoomed in before you can edit a views contents. Double-clicking a zoomed out view will cause the editor to zoom in for editing. The new view will appear in the left-side listing as View Controller Scene. Select ViewController and open the Identity Inspector. Change the class to ThirdViewController:

Next, select a TabBarItem from the object list:

And drag it onto the view:

When you drop the item, the display of the view will change to include a simulated TabBar, and position the new item in the centre of the bar:

Nicolaas tenBroek (AMDG)

198

iOS Development

In-App Navigation

The positioning is not important as this is only a simulation. Change the title of the item to Dogpile, and then add and wire-up a web view. The last step is to connect this new view to the Tab Bar Controller. Click on the view labelled Tab Bar Controller and the bar below it will change to display the First Responder and the View Controller:

Next, hold the Control key down and click on the controller. Drag over to the new view to wire-up the connection:

When you release the mouse a pop-up will appear. Choose Relationship viewControllers from the list of options:

When the selection is complete, the Tab Bar Controllers view will change to display the Dogpile controller, and an arrow will appear connecting the views. Your Storyboard should now resemble this setup:

Nicolaas tenBroek (AMDG)

199

iOS Development

In-App Navigation

With that last change the app is complete! The Storyboard automates much of the effort for an app like this, so there isnt much for you to do.

Nicolaas tenBroek (AMDG)

200

iOS Development

In-App Navigation

Page-Based Application
The Page-Based Application was introduced in iOS 5 and is modelled after a book. The app typically will need only a single view prototype, and each new view presented simply populates a new copy of the prototype with appropriate data. In addition, the views are displayed either one or two at a time, with transitions between the views animated to simulate a turning a page. While this design seems like a good idea, and we can easily imagine apps that could benefit from a setup like this, in implementation the UIPageViewController has a rather major flaw. Each new page presented is a full and real new view and controller creation, stored in an array and reused only when the original view is displayed again (i.e. if the user navigates back to that view). Obviously, if we were to create an app that displayed a book using a setup like this, we would quickly consume far more resources than would be prudent, and this even though the setup of each page was exactly the same. Yet supporting that sort of app seems to be exactly what the UIPageVIewController was designed to do. This flaw is a bit surprising given that the UITableViewController has already solved this same issue by creating a queue of reusable views and simply asking a delegate to populate the views as they are displayed. If the UIPageViewController were to adopt the reusable view approach, a maximum of four views would need to be created if the app is displaying two at a time or only two views if the app is displaying only one at a time. The extra views would be necessary to display the new pages during the animation when the user can see both part of the old view and part of the new view. Thankfully, this problem is easy enough to solve (so easy in fact, that one is forced to wonder that it has not been implemented already). Before we attempt to solve the problem, we should take some time to actually explore the operation of the UIPageViewController, and learn how it operates by creating a simple one-year monthly calendar display. At the moment iOS does not provide a true calendar control, so knowing how to create one can be quite useful, and it is not nearly as daunting as one might think. We will also take this opportunity to learn some new techniques for working with IBOutlet properties. Begin by creating a new project using the Page-Based Application template.

The second screen in the project creation process presents a rather odd view. We do not have the option of not using the Storyboard with this template.

Nicolaas tenBroek (AMDG)

201

iOS Development

In-App Navigation

This is not to say that you must use the Storyboard in order to create a Page-Based app, only that no template is provided. If you do not wish to use the Storyboard with your Page-Based app you will need to use the Single View Application template and work from there. Fortunately a Page-Based app is intended to have only one view, so creating this kind of app from scratch is not really as onerous as it may sound. The app template provides three classes for us: RootViewController, ModelController, and DataViewController. The DataViewController is responsible for the views the user will see. It populates all the data presented on the screen and is essentially a standard controller. The RootViewController is one we will not need to modify it for this app. The RootViewController is ostensibly responsible for keeping track of our location in the app and providing new views upon request. Those requests are passed on to the ModelController that then creates a new view, populates it with data, and returns it to be displayed. We will begin coding this example in the DataViewController class. Most of what the template provides for this class will need to be replaced, so you can remove everything in the header and source files. The code presented here will assume that none of the template code remains. DataViewController.h
#import <UIKit/UIKit.h> @interface DataViewController : UIViewController @property (nonatomic, strong) NSString *monthName; @property (nonatomic, strong) NSDateComponents *dateComponents; @property (nonatomic, strong) NSArray *dayNames; @property (nonatomic, weak) IBOutlet UILabel *calendarLabel; @property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *dateLabels; @property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *weekdayLabels; @end

Nicolaas tenBroek (AMDG)

202

iOS Development

In-App Navigation

Most of the setup in the header is ordinary, the exception being the last two property declarations (in bold above). The IBOutletCollection(UI_Component_Type_Name) property statement is used to create a single IBOutlet property which can reference a large number of controls. While there is not a tremendous need for this kind of outlet, it is perfect for situations in which you are dealing with a large number of controls or need to apply actions repeatedly across controls. In our situation we will have a series of seven labels for displaying the names of days, and forty-two additional labels for displaying dates. Using individual IBOutlet properties for a setup like this would be a good way to ensure the onset of stress-induced illnesses. IBOutletCollection properties are always associated with arrays. The arrays are automatically populated with the wired controls, but they need to be associated with actual array instance variables, and that means we must also use the strong attribute (or retain if you are not using ARC). UI Controls are stored in the view, and are maintained (strongly) there, so we are able to use a weak attribute for normal outlets. In this case the labels are stored (strongly) in the view, but the arrays are stored in the controller, and must be strongly held there. We will demonstrate the process of wiring up a collection momentarily, but for now our focus will remain on the code and simply assume that the collections have been wired correctly. DataViewController.m (section 1 of 2)
#import "DataViewController.h" @implementation DataViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize calendarLabel; monthName; dateComponents; dayNames; dateLabels; weekdayLabels;

#pragma mark - View lifecycle - (NSArray *)sortLabelArray:(NSArray *)array { return [array sortedArrayUsingComparator:^NSComparisonResult(id label1, id label2) { int yDiff = [label1 center].y - [label2 center].y; if(yDiff < 0) { return NSOrderedAscending; } else if(yDiff > 0) { return NSOrderedDescending; } int xDiff = [label1 center].x - [label2 center].x; if(xDiff < 0) { return NSOrderedAscending; } else if(xDiff > 0) {

Nicolaas tenBroek (AMDG)

203

iOS Development DataViewController.m (section 1 of 2)


} }]; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //sort date labels self.dateLabels = [self sortLabelArray:self.dateLabels]; self.weekdayLabels = [self sortLabelArray:self.weekdayLabels]; } return NSOrderedDescending;

In-App Navigation

return NSOrderedSame;

In this first section of the DataViewController we begin with a rather interesting method called sortLabelArray:. We need this method because of the curious way in which IBOutletCollections are handled. Despite the fact that the individual items are stored in an array and regardless of how careful you are when you wire up the controls (this is true even if you edit the nibs XML manually), you have no control over the actual ordering of components within the array. Normally this would not be a problem, but given that we are creating a calendar, we really do want the dates displayed in numerical order. To solve this problem we will need to sort the array of labels based on their position within the view. We are sorting first on the Y axis and then on the X axis. We then call sortLabelArray: in viewDidLoad so that the arrays of weekday labels and date labels are organised before we attempt any processing. DataViewController.m (section 2 of 2)
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.calendarLabel.text = [NSString stringWithFormat:@"%@ %d", self.monthName, [self.dateComponents year]]; NSDate *firstOfMonth = [self.dateComponents date]; NSDate *lastOfPreviousMonth = [NSDate dateWithTimeInterval:-86400 sinceDate:firstOfMonth]; int daysInCurrentMonth = [[self.dateComponents calendar] rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:firstOfMonth].length; int daysInPreviousMonth = [[self.dateComponents calendar] rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:lastOfPreviousMonth].length; //populate day names based on localised calendar ordering int firstWeekday = [[self.dateComponents calendar] firstWeekday] - 1; for(int i = 0; i < [self.dayNames count]; i++) { int dayIndex = (i + firstWeekday) % [self.dayNames count]; [[self.weekdayLabels objectAtIndex:i] setText:[self.dayNames objectAtIndex:dayIndex]]; } //calculate location of day 1 in the current month

Nicolaas tenBroek (AMDG)

204

iOS Development DataViewController.m (section 2 of 2)

In-App Navigation

//and the first date to display from the previous month. //The NSDateComponents are 1 indexed, we need to convert to zero indexed for our arrays int indexOfirstDayInCurrentMonth = ([self.dateComponents weekday] [[self.dateComponents calendar] firstWeekday] + [self.dayNames count]) % [self.dayNames count]; int firstDateOfPreviousMonth = daysInPreviousMonth - indexOfirstDayInCurrentMonth + 1; //populate previous month for(int i = 0; i < indexOfirstDayInCurrentMonth; i++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", firstDateOfPreviousMonth + i]]; [[dateLabels objectAtIndex:i] setEnabled:NO]; } //populate current month for(int i = indexOfirstDayInCurrentMonth, currentDay = 1; currentDay <= daysInCurrentMonth; i++, currentDay++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", currentDay]]; } //populate next month and display if necessary BOOL hideLabels = NO; for(int i = indexOfirstDayInCurrentMonth + daysInCurrentMonth, currentDay = 1; i < [dateLabels count]; i++, currentDay++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", currentDay]]; [[dateLabels objectAtIndex:i] setEnabled:NO]; if((i % [self.dayNames count]) == 0) { hideLabels = YES; } [[dateLabels objectAtIndex:i] setHidden:hideLabels]; } @end }

In the second section of our source we implement the method viewWillAppear: and use it to setup the labels for display. For purely stylistic reasons we decided to not leave any labels blank. Instead we will populate any leading slots with the last days of the previous months and any remaining slots with the first few days of the next month. This code completes the DataViewController. The app is display-only, but if you were to provide interaction, this would be the place to do it. The data model will need some small modifications to the way it operates, but none significant enough to require changing the header file from what was provided in the app template. Nonetheless, the header file is presented here to give us an overview of the model.

Nicolaas tenBroek (AMDG)

205

iOS Development ModelController.h


#import <Foundation/Foundation.h> @class DataViewController;

In-App Navigation

@interface ModelController : NSObject <UIPageViewControllerDataSource> - (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard; - (NSUInteger)indexOfViewController:(DataViewController *)viewController; @end

Our model begins with the UIPageViewControllerDataSource which lists two optional methods: pageViewController:viewControllerAfterViewController: which returns the view to display after the indicated view or nil if no views remain, and pageViewController:viewControllerBeforeViewController: which returns the view to display before the indicated view, or nil if there are no preceding views. The page control uses these methods to determine how the app should respond to user gestures (either beginning the animation to turn a page, or no response). In addition to indicating the data source implementation, the template includes two methods: viewControllerAtIndex:storyboard: and indexOfViewController:. The first method is designed to provide a new view controller with the data for the page indicated in the index property. The Storyboard is provided so that the data model can use it to create the new controller (remember that we do not have direct access to the nib when using the Storyboard). If we were to recreate this project without the Storyboard, then we would obviously be able to drop that argument and replace the code by loading the view and controller from the nib. The indexOfViewController: is used internally within the model, and should not have been made public. Frankly, with the exception of a single call in the RootViewController, the method viewControllerAtIndex:storyboard: was also not required to be public, and with a simple bit of coding that call could have been eliminated as well. The presence of these methods in the header file violates the principles of Object Oriented Design, and is yet another indication of the design problems presented in this app template. The implementation proves a bit more than is obvious from the header, so we will examine that in sections. ModelController.m (section 1 of 4)
#import "ModelController.h" #import "DataViewController.h" @implementation ModelController { @private NSArray *monthNames; NSArray *dayNames; NSDateComponents *rootDateComponents;

Nicolaas tenBroek (AMDG)

206

iOS Development ModelController.m (section 1 of 4)


} int numMonthsInYear;

In-App Navigation

Our data model is storing a number of data items, more than is absolutely necessary actually, but follows the setup provided. Specifically, the arrays of month names and day names while necessary are truly needed only in the display and therefore could easily be stored entirely within the DataViewController. The app template as provided stores the month names in the model and uses the array to keep track of the location within the app. We simply added to the mess to avoid making too many changes in one go, but we will clean it up in the next version of the app. The NSDateComponents object gives us our base month, and is used to calculate the months to display on each view. Finally, the integer called numMonthsInYear could easily be calculated every time it is needed, but seemed to be wise to store for performance reasons. This was only a guess, and should be tested against actual metrics. If no performance gain can be measured, then removing the variable and calculating the value each time it is used would be wise. You may be wondering why we would need to even calculate the number of months in a year given that the sum hasnt changed in that last few years. There are actually several calendars available on the markets that do not contain twelve months. For instance, an Academic Calendar begins in August and ends in May. Some business calendars are designed to follow fiscal years, and some other calendars contain eighteen months. ModelController.m (section 2 of 4)
- (id)init { self = [super init]; if (self) { // Create the data model. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; monthNames = [dateFormatter monthSymbols]; dayNames = [dateFormatter shortWeekdaySymbols]; //create the root calendar NSDate *now = [NSDate date]; NSCalendar *currentCalendar = [NSCalendar currentCalendar]; rootDateComponents = [currentCalendar components: NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit fromDate:now]; [rootDateComponents setCalendar:currentCalendar]; [rootDateComponents setDay:1]; //set to first day of month numMonthsInYear = [[rootDateComponents calendar] rangeOfUnit:NSMonthCalendarUnit inUnit:NSYearCalendarUnit forDate:now].length; } return self; }

Nicolaas tenBroek (AMDG)

207

iOS Development

In-App Navigation

The init method contains the setup for our data model as expected, but obtains the information in an interesting manner. We begin by creating a date formatter. We will not be using it to format dates, but that object will provide us with access to localised names for the months and days. Here we are using it to get the month names and abbreviations for the day names (also available are the full day names and very short day names). This data should never be hard-coded as the date formatter class handles all the language translation for you. Next we pick up the current date and calendar. Our calendar will display one years worth of months beginning with the current month, so the date the app launched will serve as our starting location. The NSCalendar class method currentCalendar returns back a calendar setup according to the users system settings. A companion method called autoupdatingCurrentCalendar is also available. The difference between the two is that the calendar obtained by autoupdatingCurrentCalendar will alter its behaviour if during the app execution the user changes the setup of their system. This may or may not be a desired outcome in your app, and should be considered carefully before being used. In our app it really did not matter which one was used, and you can easily see the difference by changing out this method call and comparing runs after the app is complete. Next we use the calendar to create an NSDateComponents object. The NSDateComponents class provides methods to easily parse out interesting data from the calendar, and helps to organise that data according to the users system settings. We will be using the components to discover the number of days in each month, and which day of the week the users calendar begins on (typically Sunday or Monday, but others may also be available). When creating an NSDateComponents object you must specify the data you plan to access (using a bitwise OR to combine multiple pieces). If you access data that was not specified, the returned values are typically undefined (which translates to very, very bad). Finally, we use the NSDateComponents to discover the number of months in the calendar. The value returned by rangeOfUnit:inUnit:forDate: is actually an NSRange structure which contains the elements location and length. Location indicates the position of the passed-in date in the total, while the length gives the total number of months. ModelController.m (section 3 of 4)
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard { // Return the data view controller for the given index. if (!monthNames) { return nil; } //calculate month of new view int monthIndex = (([rootDateComponents month] + index - 1) % numMonthsInYear); // Create a new view controller and pass suitable data. DataViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:@"DataViewController"];

Nicolaas tenBroek (AMDG)

208

iOS Development ModelController.m (section 3 of 4)


dataViewController.monthName = [monthNames objectAtIndex:monthIndex]; dataViewController.dayNames = dayNames; //calculate date components for month to display NSDateComponents *tempDateComponents = [rootDateComponents copy]; [tempDateComponents setMonth:monthIndex + 1]; if([rootDateComponents month] > [tempDateComponents month]) { [tempDateComponents setYear:[tempDateComponents year] + 1]; }

In-App Navigation

dataViewController.dateComponents = [[tempDateComponents calendar] components: NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit fromDate:[tempDateComponents date]]; [dataViewController.dateComponents setCalendar:[tempDateComponents calendar]]; } return dataViewController;

- (NSUInteger)indexOfViewController:(DataViewController *)viewController { /* Return the index of the given data view controller. For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index. */ //return [monthNames indexOfObject:viewController.monthName]; return [viewController.dateComponents month] % numMonthsInYear; }

The first method in this section builds a new view controller and populates it with the components necessary to display one month. The process of creating the NSDateComponents is a bit convoluted, so we will go through it a bit carefully. We begin by copying the root date component and setting both the month and year values as appropriate. You should note that we added one to each of those components. Most of the NSDateComponents are ordinal values starting with one rather than the traditional zero. This means that we will constantly need to shift back and forth by one as we use arrays and date components. After calculating the new month and year we do something quite peculiar. We obtain the calendar and date from the temporary components, and use those objects to generate more components. Unfortunately the NSDateComponents class does not compute the values it returns at the time they are requested. Instead those values are calculated one time when the components object is created. This means that changing the month and year affects only those two values, while the values for day and weekday are unchanged and therefore invalid. The second method in this section is indexOfViewController:. We commented out the code provided by the template and replaced it with a simple computation. This method is a helper method used by the data source methods we will see momentarily.

Nicolaas tenBroek (AMDG)

209

iOS Development ModelController.m (section 4 of 4)


#pragma mark - Page View Controller Data Source

In-App Navigation

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController { NSUInteger index = [self indexOfViewController:(DataViewController *)viewController]; if ((index == 0) || (index == NSNotFound)) { return nil; } index--; return [self viewControllerAtIndex:index storyboard:viewController.storyboard];

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController { NSUInteger index = [self indexOfViewController:(DataViewController *)viewController]; if (index == NSNotFound) { return nil; } index++; if (index == [monthNames count]) { return nil; } return [self viewControllerAtIndex:index storyboard:viewController.storyboard];

@end

The last methods displayed are unchanged from the template. Their job is to implement the UIPageViewControllerDataSource protocol by providing either a view to display or nil if no more views are available. These methods complete the coding necessary for the app, which means we can now turn our attention to the design, and see how to wire up an IBOutletCollection.

Nicolaas tenBroek (AMDG)

210

iOS Development

In-App Navigation

Begin by creating seven labels for day names, and forty-two for the dates. Yes, that is quite a few labels, and thank goodness we have access to the outlet collection or this part might make us crazy.

After the labels have been created, right-click on the ViewController to open its context menu. Look for a section titled Outlet Collection (we have highlighted it in the image below):

Click and drag from the circle to the appropriate controls, just as you have in the past. In this instance you will need to click the circle and wire several controls to the same location.

Nicolaas tenBroek (AMDG)

211

iOS Development

In-App Navigation

It can be easy to get lost while wiring lots of controls, but IB can display for you which controls have been wired. Hover your mouse over the circle and all of the wired controls will be highlighted. In the image below we hovered over the circle after wiring three day labels.

Wire-up the remaining date labels and your app should run fine, beginning on the current month and then displaying the next month each time you turn the page. If you tap on the right side of the screen the next month will be displayed (although it will stop after twelve months), and tapping on the left side will display the previous month (until you get back to the original month). In addition, if you swipe slowly across the screen you will be able to control the paginating animation quite a bit. Do that slowly and you will notice that the back of the page displays the reverse image of the front, as if the content is showing through the page. This happens automatically when the UIPageViewController is set to display single-sided pages, and not when it is set to display double-sided pages.

Nicolaas tenBroek (AMDG)

212

iOS Development

In-App Navigation

Modified Page-Based Application


Now that we have seen the UIPageViewController in action, we can take just a few minutes and drastically improve the operation of the app, making it much friendlier from a system resources perspective, and a bit more functional by removing the one year restriction. We will also look at other display options like having two views on screen at the same time. DataViewController.h
#import <UIKit/UIKit.h> @interface DataViewController : UIViewController @property (nonatomic, strong) NSDateComponents *dateComponents; @property (nonatomic, weak) IBOutlet UILabel *calendarLabel; @property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *dateLabels; @property (nonatomic, strong) IBOutletCollection(UILabel) NSArray *weekdayLabels; @end

We will begin by stripping down the DataViewControllers header file to the bare minimum necessary. We have removed the month and day names properties and will instead store those values privately within the DataViewController. DataViewController.m (section 1 of 2)
#import "DataViewController.h" @implementation DataViewController static NSArray *monthNames; static NSArray *dayNames; @synthesize @synthesize @synthesize @synthesize calendarLabel; dateComponents; dateLabels; weekdayLabels;

#pragma mark - View lifecycle - (NSArray *)sortLabelArray:(NSArray *)array { return [array sortedArrayUsingComparator:^NSComparisonResult(id label1, id label2) { int yDiff = [label1 center].y - [label2 center].y; if(yDiff < 0) { return NSOrderedAscending; } else if(yDiff > 0) { return NSOrderedDescending; } int xDiff = [label1 center].x - [label2 center].x;

Nicolaas tenBroek (AMDG)

213

iOS Development DataViewController.m (section 1 of 2)


if(xDiff < 0) { return NSOrderedAscending; } else if(xDiff > 0) { return NSOrderedDescending; } return NSOrderedSame; } }];

In-App Navigation

- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //sort date labels self.dateLabels = [self sortLabelArray:self.dateLabels]; self.weekdayLabels = [self sortLabelArray:self.weekdayLabels]; if(!monthNames) { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; monthNames = [dateFormatter monthSymbols]; dayNames = [dateFormatter shortWeekdaySymbols]; } }

Not much changed in this section of code, and the few lines that did are displayed in a bold font. We removed the synthesized properties that are no longer valid and added static arrays for month names and day names. Then in the viewDidLoad method we populated those arrays only when they did not already exist. Using static here ensures that each instance of the controller has access to the same variables, and thus they need to be created only the first time they are accessed. DataViewController.m (section 2 of 2)
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.calendarLabel.text = [NSString stringWithFormat:@"%@ %d", [monthNames objectAtIndex:([self.dateComponents month] - 1) % [monthNames count]], [self.dateComponents year]]; NSDate *firstOfMonth = [self.dateComponents date]; NSDate *lastOfPreviousMonth = [NSDate dateWithTimeInterval:-86400 sinceDate:firstOfMonth]; int daysInCurrentMonth = [[self.dateComponents calendar] rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:firstOfMonth].length; int daysInPreviousMonth = [[self.dateComponents calendar] rangeOfUnit:NSDayCalendarUnit inUnit:NSMonthCalendarUnit forDate:lastOfPreviousMonth].length; //populate day names based on localised calendar ordering int firstWeekday = [[self.dateComponents calendar] firstWeekday] - 1;

Nicolaas tenBroek (AMDG)

214

iOS Development DataViewController.m (section 2 of 2)

In-App Navigation

for(int i = 0; i < [dayNames count]; i++) { int dayIndex = (i + firstWeekday) % [dayNames count]; [[self.weekdayLabels objectAtIndex:i] setText:[dayNames objectAtIndex:dayIndex]]; } //calculate location of day 1 in the current month //and the first date to display from the previous month. //The NSDateComponents are 1 indexed, we need to convert to zero indexed for our arrays int indexOfirstDayInCurrentMonth = ([self.dateComponents weekday] [[self.dateComponents calendar] firstWeekday] + [dayNames count]) % [dayNames count]; int firstDateOfPreviousMonth = daysInPreviousMonth - indexOfirstDayInCurrentMonth + 1; //populate previous month for(int i = 0; i < indexOfirstDayInCurrentMonth; i++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", firstDateOfPreviousMonth + i]]; [[dateLabels objectAtIndex:i] setEnabled:NO]; } //populate current month for(int i = indexOfirstDayInCurrentMonth, currentDay = 1; currentDay <= daysInCurrentMonth; i++, currentDay++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", currentDay]]; [[dateLabels objectAtIndex:i] setEnabled:YES]; [[dateLabels objectAtIndex:i] setHidden:NO]; } //populate next month and display if necessary BOOL hideLabels = NO; for(int i = indexOfirstDayInCurrentMonth + daysInCurrentMonth, currentDay = 1; i < [dateLabels count]; i++, currentDay++) { [[dateLabels objectAtIndex:i] setText:[NSString stringWithFormat:@"%d", currentDay]]; [[dateLabels objectAtIndex:i] setEnabled:NO]; if((i % [dayNames count]) == 0) { hideLabels = YES; } } } @end [[dateLabels objectAtIndex:i] setHidden:hideLabels];

Not much changed in the second section of code either. We simply replaced all references to the month and day name properties with references to the static arrays. Again all of the changes are bolded, and no changes were made to the functionality of this class.

Nicolaas tenBroek (AMDG)

215

iOS Development ModelController.h


#import <Foundation/Foundation.h> @class DataViewController; @interface ModelController : NSObject <UIPageViewControllerDataSource> - (id)initWithStoryboard:(UIStoryboard *)storyboard; @end

In-App Navigation

The changes to the data model are a bit more drastic than what was needed in the controller. First we removed the methods that should have been private and then added an init method which takes a Storyboard argument. In this modification we will be creating a small number of views and then reusing them, so the init method is the only place that will need access to the Storyboard. ModelController.m (section 1 of 4)
#import "ModelController.h" #import "DataViewController.h" @implementation ModelController { @private NSDateComponents *rootDateComponents; int numMonthsInYear; NSArray *storedDataViewControllers; }

In this first section of code we have reduced the private data by removing the arrays of day and month names. We have refactored the code properly and located those items where they are used, reducing the coupling between the model and controller. In addition to removing the unnecessary data we added a new array that will be used to hold the controllers and provide them for reuse as necessary. ModelController.m (section 2 of 4)
- (id)initWithStoryboard:(UIStoryboard *)storyboard { if (self = [super init]) { //create the root calendar NSDate *now = [NSDate date]; NSCalendar *currentCalendar = [NSCalendar autoupdatingCurrentCalendar]; rootDateComponents = [currentCalendar components: NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit fromDate:now]; [rootDateComponents setCalendar:currentCalendar]; [rootDateComponents setDay:1]; //set to first day of month numMonthsInYear = [[rootDateComponents calendar] rangeOfUnit:NSMonthCalendarUnit inUnit:NSYearCalendarUnit forDate:now].length;

Nicolaas tenBroek (AMDG)

216

iOS Development ModelController.m (section 2 of 4)

In-App Navigation

storedDataViewControllers = [[NSMutableArray alloc] init]; //the UIPageViewController will either need one or two views depending //on the location of the spine //we need to double the number required so that new views can be //displayed on the screen while the old ones are animating off for(int i = 0; i < 4; i++) { [(NSMutableArray *)storedDataViewControllers addObject: [storyboard instantiateViewControllerWithIdentifier:@"DataViewController"]]; } } } return self;

The new init method is slightly different from its predecessor. We removed the creation of the month and day names arrays and added in the creation of an array of four view controllers. We generally think that the UIPageViewController is capable of displaying either one or two views at a time, but during the process of animating old views off the screen and new ones on, twice that number are needed. Therefore we need at most four views, though you only need that many if you plan to support the display of two views. If you plan to only support one view on the screen at a time then two views would suffice. ModelController.m (section 3 of 4)
- (DataViewController *)viewControllerAtMonth:(NSUInteger)month withYear:(NSInteger)year { DataViewController *dataViewController = [storedDataViewControllers objectAtIndex:(month % [storedDataViewControllers count])]; NSDateComponents *tempDateComponents = [rootDateComponents copy]; [tempDateComponents setMonth:month]; [tempDateComponents setYear:year]; dataViewController.dateComponents = [[tempDateComponents calendar] components: NSYearCalendarUnit|NSMonthCalendarUnit|NSDayCalendarUnit|NSWeekdayCalendarUnit fromDate:[tempDateComponents date]]; [dataViewController.dateComponents setCalendar:[tempDateComponents calendar]]; return dataViewController; }

In section three we made some small but very significant changes. The method indexOfViewController: was no longer needed, so it has been removed. In addition, the method viewControllerAtIndex: has been renamed to viewControllerAtMonth:withYear:, and its operation is now greatly simplified from the previous version. We are no longer attempting to calculate the month and year (as they are now arguments), the references to the month and day names have been removed, and we also removed the Storyboard access from the method, and replaced the line that created a new controller with one that

Nicolaas tenBroek (AMDG)

217

iOS Development

In-App Navigation

simply retrieves an existing one from the array. So much of this method changed that we did not bother to bold the modifications. This is instead a wholesale replacement of two methods with a much simplified one. ModelController.m (section 4 of 4)
#pragma mark - Page View Controller Data Source - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController { NSInteger month = [[(DataViewController *)viewController dateComponents] month] - 1; int year = [[(DataViewController *)viewController dateComponents] year]; if(month <= 0) { month = numMonthsInYear; year--; } return [self viewControllerAtMonth:month withYear:year]; } - (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController { NSInteger month = [rootDateComponents month]; int year = [rootDateComponents year]; if (viewController) { month = [[(DataViewController *)viewController dateComponents] month] + 1; year = [[(DataViewController *)viewController dateComponents] year]; } if(month > numMonthsInYear) { month = 1; year++; } } @end return [self viewControllerAtMonth:month withYear:year];

In this final section of the data models code we made significant changes to both pageViewController:viewControllerBeforeViewController: and pageViewController:viewControllerAfterViewController:. Rather than calculating an index to figure out where the next control will come from we now use some very simple logic to calculate the month and year of the next (or previous) calendar, and then pass those new values on to viewControllerAtMonthWithYear:. After this rewrite the code is more resource friendly and the logic in these last three methods is greatly simplified over the previous approach while providing a more functional calendar. That combination is a pretty big programming win for us.

Nicolaas tenBroek (AMDG)

218

iOS Development RootViewController.m (section 1 of 2)


#import "RootViewController.h" #import "ModelController.h" #import "DataViewController.h" @implementation RootViewController { @private ModelController *modelController; } @synthesize pageViewController; #pragma mark - View lifecycle

In-App Navigation

- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // Configure the page view controller and add it as a child view controller. self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; self.pageViewController.delegate = self; modelController = [[ModelController alloc] initWithStoryboard:self.storyboard]; // DataViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard]; NSArray *viewControllers = [NSArray arrayWithObject: [modelController pageViewController:self.pageViewController viewControllerAfterViewController:nil]]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL]; self.pageViewController.dataSource = modelController; [self addChildViewController:self.pageViewController]; [self.view addSubview:self.pageViewController.view]; //Set the page view controller's bounds using an inset rect //so that self's view is visible around the edges of the pages. CGRect pageViewRect = self.view.bounds; self.pageViewController.view.frame = pageViewRect; [self.pageViewController didMoveToParentViewController:self]; // Add the page view controller's gesture recognizers to the book view controller's view //so that the gestures are started more easily. self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;

Nicolaas tenBroek (AMDG)

219

iOS Development

In-App Navigation

To make the system work with our new resource-friendly data model we need to make some small changes to the RootViewControllers viewDidLoad method. First, we modified the init method used when creating the data model so that we now call the new initWithStoryboard method. Next we removed the line which created the startingViewController (though we left it commented out for you to see), and finally modified the line creating the array of view controllers to use pageViewController:viewControllerAfterViewController: to retrieve the first months view. With those changes in place you can run the app and it should appear to work exactly the same way as it did before. All the differences are under the hood, and we can rest comfortably knowing that our app is much more resource friendly than it was previously. Now that we have fixed the problems inherent in the app template, we can take a moment to explore a bit more about the UIPageViewController. As you know, the page view controller operates like a book, and like a book it has a spine. The spines location determines how the page turning happens and can be positioned on an edge of the screen (left or right for a horizontal orientation and top or bottom for a vertical orientation) or in the middle. If the spine is in the middle of the screen two views will be visible at a time. With some quick modifications we can make that happen, though the results for our calendar will not be spectacular. RootViewController.m (section 2 of 2)
#pragma mark - UIPageViewController delegate methods /* - (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed { } */ - (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation { if(orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { //Set the spine position to "min" and the page view controller's view controllers array to //contain just one view controller. Setting the spine position to //'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided //property to YES, so set it to NO here. self.pageViewController.doubleSided = NO; return UIPageViewControllerSpineLocationMin; } else { UIViewController *vc0 = ([self.pageViewController.viewControllers count] >= 1)? [self.pageViewController.viewControllers objectAtIndex:0] : [modelController pageViewController:self.pageViewController viewControllerAfterViewController:nil]; UIViewController *vc1 = ([self.pageViewController.viewControllers count] >= 2)? [self.pageViewController.viewControllers objectAtIndex:1] : [modelController pageViewController:self.pageViewController

Nicolaas tenBroek (AMDG)

220

iOS Development RootViewController.m (section 2 of 2)

In-App Navigation

viewControllerAfterViewController:vc0]; NSArray *viewControllers = [NSArray arrayWithObjects:vc0, vc1, nil]; [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL]; //self.pageViewController.doubleSided = YES; - this is automatically set return UIPageViewControllerSpineLocationMid; } @end }

In this method we simply check to see the orientation of the interface and set the spine to either the left side if the interface is in a portrait mode or in the middle if the interface is in a landscape mode. In order to display two views at a time, we need to ensure the UIPageViewController has two views. With some quick checks we can create a new array using either existing or new views and pass that to the controller. With these changes in place run the app and you should see something like the following image that is both good and bad at the same time:

If we change the navigation orientation (in viewDidLoad) to UIPageViewControllerNavigationOrientationVertical, we would see the following display:

Nicolaas tenBroek (AMDG)

221

iOS Development

In-App Navigation

Obviously, if we wanted to use either of these displays we would need to make new views or massively modify the one we do have.

Nicolaas tenBroek (AMDG)

222

iOS Development

In-App Navigation

Navigation Controller
Navigation of a hierarchical application is handled through instances of UINavigationController. Navigation is managed using a stack of views, with the view at the top of the stack being the one displayed on the screen. The bottom of the stack (or first view) is called the Root View Controller. New views are added to the stack through a push operation, and a pop operation removes them from the stack. In code then, navigation is nothing more than a series of push and pop operations that are most often triggered by user actions. The UINavigationController is a subclass of UIViewController, but we do not use it the same way we have been using UIViewController. All of our examples up to this point have created a subclass of UIViewController for each screen that we want displayed. Screens for navigation-based apps must continue that process. The process change in navigation apps is that the UIViewController subclasses are given to the UINavigationController for display. Within this setup new views will be added to the screen in much the same way as we have been adding controls to our screens up to this point. As you can see from the image below, the UINavigationController divides the screen space into three main sections, each with their own UIVew. At the top is a view called the navigation bar, in the middle is a view for displaying content, and at the bottom is a view called a toolbar. The content view is the space reserved for your apps content, so that area will change depending on the screen being displayed. While designing your screens you must take the reduced screen space into account. You can design the screens using simulated navigation bars and tool bars as we did in the previous example with the tab bar, or by setting the view to have a flexible size by setting the autoresizingMask, or both. The autoresizingMask property is set to UIViewAutoResizingNone by default, and in a navigation-based application you would typically change this to UIViewAutoresizingFlexibleHeight. Many navigationbased apps hide the bottom toolbar until it is needed, and then display it only at the moment it is needed. Having a flexible display is the best approach to handle that kind of situation and ensures each screen has the most screen space possible.

The top navigation bar is an instance of UINavigationBar and is also broken into three sections. The left and right sides of the bar can display UIBarButtonItems. UIBarButtonItem instances are simply buttons

Nicolaas tenBroek (AMDG)

223

iOS Development

In-App Navigation

with a custom look to match the navigation bar. The buttons are accessed through the properties leftBarButtonItem and rightBarButtonItem respectively. The middle section is called the Title View and is typically used to display the title of a screen as a string, but can be replaced with a custom view. The Title View can be accessed through the titleView property if you wish to use a custom view or through the title property if you only need to set the title as a string. The navigation controller updates the navigation bar each time a view controller is displayed (i.e. each time there is a push or pop operation) and the rules for updating each section are different. If the new view controller does not set the right bar button item property, then nothing is displayed in that space. If the new view controller does not set the title or title view properties of the navigation bar, then the navigation bar displays the contents of the new view controllers title property (yes, UINavigationBar and UIViewController each have a property called title). If neither title property has been set, then nothing will be displayed in the centre of the navigation bar. The rules for updating the left bar button item are more complex. If the view controller being displayed is the root view controller and the root does not place a custom instance of UIBarButtonItem in the left bar button property, then nothing will be displayed in that section. If the view controller being displayed is not the root view controller, then the rules are entirely different. Each time a new view controller is pushed on to the stack the left bar button item will update. If the new view controller explicitly sets the left bar button item, then that new button is displayed. If the new view controller does not set the left bar button item, and a previous view controller set the navigation controllers backBarButtonItem property, then that same button is displayed again. If the new view controller does not set the left bar button item and no previous views have set the navigation controllers backBarButtonItem property, then a default back button is displayed with the previous view controllers title. Of course, if the previous view controller did not set its title property, then the back button cannot be displayed, resulting in empty space on the left side of the navigation bar. The toolbar is a different matter altogether. It is hidden by default, but can by displayed at any time by calling setToolbarHidden:animated:. The tool bar self-populates when it is displayed using the current view controllers toolbarItems property, which is simply an array of UIBarButtonItems. You can set the property directly with a new array, or if the toolbar is already displayed use the setToolbarItems:animated: method to animate the change in available buttons. Thankfully, the toolbar is simpler than the navigation bar as one screens buttons have no effect on what the toolbar will display for other screens. If you plan to use the toolbar in all screens, you can simply reveal it once at the beginning of your app and then leave it up. You can reveal the toolbar through code either in the App Delegate or in the root view controller, or you can reveal it at design time through Interface Builder. To reveal it using Interface Builder, simply open the appropriate nib or Storyboard, select the navigation controller, and enable Shows Toolbar in the Attributes Inspector. Now that we have covered the parts and purpose of a navigation controller, we can write some code to demonstrate its use. For this project sample we will develop a simple shopping list app with two screens. The first screen will show the list and allow for basic list editing, while the second screen will allow us to add items to the list.

Nicolaas tenBroek (AMDG)

224

iOS Development

In-App Navigation

In previous versions of XCode we could start a project like this by using a Navigation-based Application template; sadly that option was removed in XCode version 4.2 (for no apparent reason). Do not take the lack of a template as an indication that this style of app is being phased out. The design continues to be extremely important, and all of the necessary parts are still supported. With any luck the template will reappear in the future, though the setup is so simple that a template is not really necessary. Begin by creating an Empty Application:

Name the app ShoppingList, and note the lack of an option for using the Storyboard:

We will examine how to use the Storyboard with NavigationControllers immediately after we develop this project so that the approaches can be compared. Take a brief moment to examine the project. This setup is as basic as it can be, providing a bare App Delegate and nothing else, a clean slate for us to work with. Next right-click on the ShoppingList folder and choose New File (or use the File->New->New File menu if you prefer), select the UIViewController subclass and press Next. Name the class RootViewController, ensure that it is a

Nicolaas tenBroek (AMDG)

225

iOS Development

In-App Navigation

subclass of UITableViewController and that the With XIB for user interface option is selected. After the files are created we need to make some small changes to the App Delegate. AppDelegate.m
#import "AppDelegate.h" #import "RootViewController.h" @implementation AppDelegate { @private RootViewController *rootViewController; UINavigationController *navigationController; } @synthesize window = _window; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.window.backgroundColor = [UIColor whiteColor]; rootViewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil]; navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; [self.window addSubview:navigationController.view]; [self.window makeKeyAndVisible]; return YES; } /* Remaining methods are unchanged from the template and have been removed from this listing for brevity */ @end

Begin the changes by importing our newly minted RootViewController header and creating two instance variables to hold our navigation controller and root view controller. Next, in the method application:didFinishLaunchingWithOptions: we need to add a few lines of code. We create instances of the root view controller and navigation controller, passing a reference to the root view controller to the navigation controller. The next step is to add the navigation controller to the window. From this point on the navigation controller will take responsibility for displaying all views and will display them as subviews of itself. While we have not done any real work yet, we can run our app and see the results of these two lines of code. Run the app and notice that the empty table is displayed below an empty blue navigation bar, which means our setup is good and we can proceed.

Nicolaas tenBroek (AMDG)

226

iOS Development

In-App Navigation

We shall start the real work by creating our data class. Create a new Objective-C class called ShoppingListItem and subclassing NSObject. ShoppingListItem.h
#import <Foundation/Foundation.h> @interface ShoppingListItem : NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) double quantity; + (BOOL)isValidName:(NSString *)proposedName; + (BOOL)isValidQuantity:(double)proposedQuantity; @end

As you can see, this class is quite simple. We do not really need much behaviour from our shopping list item at this point, holding the name of the item and the quantity we need should be enough. Notice we also added two class methods (indicated by the plus signs) to provide validation of the name and quantity. In order to use these validation methods we must override the default setter methods. All together then we have a total of four methods to write in the implementation. We decided to expose our data validation methods as class methods so that we could call them at a later point without creating an instance of the class. You will see this process shortly. For now, our

Nicolaas tenBroek (AMDG)

227

iOS Development

In-App Navigation

validation is quite simple. The name must not be nil or an empty string. The quantity must be a positive value within the range of numbers that a double can store (i.e. not infinity). ShoppingListItem.m (section 1 of 2)
#import "ShoppingListItem.h" @implementation ShoppingListItem @synthesize name; @synthesize quantity; + (BOOL)isValidName:(NSString *)proposedName { return ((proposedName != nil) && ([proposedName length] > 0)); } + (BOOL)isValidQuantity:(double)proposedQuantity { return ((proposedQuantity != HUGE_VAL) && (proposedQuantity != -HUGE_VAL) && (proposedQuantity >= 0.0)); }

Next we need to override the setter methods. There really is not much we can do in them other than validate the data. If the passed-in data is valid we will store the new data, and if not we will ignore it. Remember that in every setter for a reference we must first free the old reference and then retain the new one. We also need to override the dealloc method so we can release the item name. ShoppingListItem.m (section 2 of 2)
- (void)setName:(NSString *)proposedName { if([ShoppingListItem isValidName:proposedName]) { name = proposedName; } } - (void)setQuantity:(double)proposedQuantity { if([ShoppingListItem isValidQuantity:proposedQuantity]) { quantity = proposedQuantity; } } @end

In our previous table examples we pre-programmed all the table data. Obviously a pre-programmed shopping list would not be terribly useful, so before we deal with the display of the shopping list, we will create a view controller that will allow us to create new items to place on the list. Start this step by adding a UIViewController subclass called CreateListItemViewController to your app. As our shopping list items have only two pieces of data, this view will be quite simple with two text fields. We also need

Nicolaas tenBroek (AMDG)

228

iOS Development

In-App Navigation

some way to notify the root view controller that it is time to add the newly created item to the list, so we will create a protocol as well. CreateListItemViewController.h
#import <UIKit/UIKit.h> #import "ShoppingListItem.h" @protocol CreateItemDelegate; @interface CreateListItemViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) IBOutlet UITextField *nameTextField; @property (nonatomic, weak) IBOutlet UITextField *quantityTextField; @property (nonatomic, weak) id<CreateItemDelegate> delegate; @end @protocol CreateItemDelegate - (void)addShoppingListItem:(ShoppingListItem *)newItem; @end

You should be able to recognise the pattern in this class. It is the same one that was used in the Flashlight app to notify the main screen of the done button press. Here we have a delegate that implements the CreateItemDelegate protocol. We can then inform the delegate to add an item to the shopping list by calling the protocol method addShoppingListItem:. You likely also noticed that while we have IBOutlets for our text fields, we do not have an IBAction method for a done or save type button press. Obviously we do need a way for the user to inform the app that they have entered the data and are ready to save it, but as this is a navigation-based app, we will handle that process entirely within the implementation. The interface is complete, so we can now turn our attention to the implementation. Despite the simplicity of the interface we will need to add several methods, so we shall examine them each in turn. Recall that the navigation bar offers a third section (the right side) where we can easily place a button for the current screen. This is an ideal location for our save button and we shall add that momentarily. Before we add the button we need a method that the button can call. Lets start by writing that method. We titled ours with the wonderfully descriptive name saveButtonPressed. In this method we validate our two pieces of data by calling the class-level data validation methods we wrote previously. When the data is invalid then we need to alert the user and stop the save process. When the data is valid we create a new ShoppingListItem object, and pass it to our delegate for inclusion in the list. We then release the item (the list will have to retain it), and tell the navigation controller to dispose of this view with a call to popViewControllerAnimated:.

Nicolaas tenBroek (AMDG)

229

iOS Development CreateListItemViewController.m (section 1 of 3)


#import "CreateListItemViewController.h" @implementation CreateListItemViewController @synthesize nameTextField; @synthesize quantityTextField; @synthesize delegate; #pragma mark -

In-App Navigation

- (void)saveButtonPressed { //validate name if(![ShoppingListItem isValidName:nameTextField.text]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Item Name Missing" message:@"Please enter a name for this item" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; return; } //validate quantity double quantity = [quantityTextField.text doubleValue]; if(![ShoppingListItem isValidQuantity:quantity]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Invalid Quantity" message: [NSString stringWithFormat:@"Please adjust quantity.\nQuantity must be a number greater than 0 and less than %lf", HUGE_VAL] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; return; } ShoppingListItem *item = [[ShoppingListItem alloc] init]; item.name = nameTextField.text; item.quantity = quantity; [delegate addShoppingListItem:item]; [self.navigationController popViewControllerAnimated:YES];

Next we need to handle the text field delegate methods (did you notice that in the header file we implemented the UITextFieldDelegate?). We chose to override textFieldShouldReturn: to provide some nice behaviour in response to the return button being pressed. Unfortunately both fields call the same method (thats not proper Object Oriented Design at all!), so we need to compare the passed-in text field to our properties in order to figure out which field made the call. Thankfully we have only two fields, so it will not be hard to figure out which one we are dealing with. If the user is typing in the item name and presses return, then we switch focus to the quantity field. If the user is typing in the quantity and presses return, then we treat that action as if they had pressed the save button. Attention to the user interaction is extremely important in mobile apps. Remember that users are not Nicolaas tenBroek (AMDG) 230

iOS Development

In-App Navigation

always using their devices in an ideal environment. Anything we can do for them in code will make their use of our app more enjoyable and ultimately will make the app more useful for them. This tiny bit of code will go a long way on that front. CreateListItemViewController.m (section 2 of 3)
#pragma mark - UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { if(textField == nameTextField) { [quantityTextField becomeFirstResponder]; } else { [self saveButtonPressed]; } return YES; }

Now we can turn our attention to the viewDidLoad method. Here we need to set the views title and create our save button. Remember that the title will automatically appear in the centre of the navigation bar as long as we have set the views title property. When creating any bar button you need to keep in mind that the UIBarButtonItem class contains a large number of pre-built buttons. Using them makes the app look more like part of the system as a whole, so user them whenever you can. In this case we can use the pre-built save button. We indicate that the current object is the delegate and create a selector for the method to call when the button is pressed. Then we add the button to the right side of the navigation bar. Remember that the left side will have a back button, so the right side is where we want the new button. Finally we will overload viewDidAppear: to ensure the name text field becomes the first responder. This saves yet another tap on the users part, and makes our app that much friendlier. CreateListItemViewController.m (section 3 of 3)
#pragma mark - UIViewController - (void)viewDidLoad { [super viewDidLoad]; self.title = @"New Item"; UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveButtonPressed)]; self.navigationItem.rightBarButtonItem = saveButton;

- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [nameTextField becomeFirstResponder]; }

Nicolaas tenBroek (AMDG)

231

iOS Development CreateListItemViewController.m (section 3 of 3)


@end

In-App Navigation

With the code for this view controller complete, we can turn our attention to the design of the view. Open the nib, select the view, and select the Navigation Bar as the Top Bar of the Simulated Metrics. This will help us to align the other controls properly.

Now add labels and text fields for our two properties and wire up the outlets and delegates.

Nicolaas tenBroek (AMDG)

232

iOS Development

In-App Navigation

Once the items are placed and wired up, we need to make some decisions about the keyboard appearance and text field behaviours. The name text field should probably capitalise words and spellcheck, as most of the items on it will be real words and product names. We should also change the text of the return key to Next, so the users know that when they press it focus will shift to the quantity text field. There is no text in this field by default, but we can enter a placeholder prompt. We can also enable the clear button to make deleting the entire field easy if the user changes their mind about the entry. (The modified fields have been highlighted in the following image for easier identification.)

The quantity text field will require different settings than the name text field. The data in the field should be numbers, and we should never have a quantity less than one. With that in mind we can prefill the text field with a 1, turn off capitalisation and spelling, and change the keyboard to Numbers and Punctuation. Additionally, as there are no more fields after this one, we can change the return key to Done. The Done button also signifies to the user that the edits will be saved, which we handled in our code already.

Nicolaas tenBroek (AMDG)

233

iOS Development

In-App Navigation

Next we can turn our attention to the shopping list itself. We need to indicate that the root controller implements the CreateItemDelegate protocol so that it can be notified when the user has created a new shopping list item. Other than that, no changes are required in the header. RootViewController.h
#import <UIKit/UIKit.h> #import "CreateListItemViewController.h" @interface RootViewController : UITableViewController <CreateItemDelegate> @end

There is quite a bit of code in this class, but much of it has been written for us as part of the template. We will again examine each method in turn, but before we get to them we need to make a decision about our data model. Just as we did in previous examples, we will use an array for the model. If we wanted to categorise our list, we may later decide to use something else, but at this moment keeping things simple seems a better approach. The first method in our listing is createShoppingListItem. This is a method we created and will be called when the user presses the add button on the navigation bar (we will take care of creating that button in just a bit). In this method we create a new CreateListItemViewController, set the root view controller as the delegate, and then ask the navigation controller to display the new view with a push operation. RootViewController.m (section 1 of 4)
#import "RootViewController.h" #import "ShoppingListItem.h" @implementation RootViewController { @private NSMutableArray *shoppingList; } #pragma mark - (void)createShoppingListItem { CreateListItemViewController *view = [[CreateListItemViewController alloc] initWithNibName:@"CreateListItemViewController" bundle:nil]; view.delegate = self; [self.navigationController pushViewController:view animated:YES]; }

Next, we implement the CreateItemDelegate by writing addShoppingListItem:. This method is very simple, as we need only to add the new item to our array and then ask the table to reload. Remember that adding an item to an array incurs a retain operation, and in our last class we released the item

Nicolaas tenBroek (AMDG)

234

iOS Development

In-App Navigation

after calling this method. That means the value of the retain counter is one after both methods complete. So we have no memory leaks here. RootViewController.m (section 2 of 4)
#pragma mark - CreateItemDelegate - (void)addShoppingListItem:(ShoppingListItem *)newItem { [shoppingList addObject:newItem]; [self.tableView reloadData]; }

Next we can turn our attention to initWithNibName:bundle:. Here we create the array that will serve as our data model. Everything else we need to build will be part of the view, so that is all this method needs to do. In viewDidLoad we create two UIBarButtonItems. This view controller does not need a back button, as it is the root of the navigation, so we can safely put one button on the left and another on the right. The right button will be our add button (which we mentioned earlier), and as before we can use a pre-built button. The left button will be used for placing the table into edit mode. The UITableViewController actually contains a pre-built edit button, so we need only to add it to the left side. This pre-built edit button displays Edit until the user presses it, and then it changes to a system Done button. Additionally, it will automatically toggle the tables edit mode. (You should recall that we manually implemented this same feature in an earlier example.) RootViewController.m (section 3 of 4)
#pragma mark - UIViewController - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if((self = [super initWithNibName:nibName bundle:bundle])) { shoppingList = [[NSMutableArray alloc] init]; } return self; } - (void)viewDidLoad { [super viewDidLoad]; UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(createShoppingListItem)]; self.navigationItem.rightBarButtonItem = addButton; } self.navigationItem.leftBarButtonItem = self.editButtonItem;

Nicolaas tenBroek (AMDG)

235

iOS Development

In-App Navigation

You are already familiar with the table data source and delegate methods we need to write. Within those the numberOfSectionsInTableView: and tableView:numberOfRowsInSection: are pretty standard, so we will skip over those and take a quick look at tableView:cellForRowAtIndexPath:. Actually, there isnt much unusual here either, but we have used one of the built-in cell style models rather than creating a custom cell. We used style 1 which offers a left-justified black label on the left side and a right-justified blue label on the right side. We then put the item name in the black label and the quantity in the blue label. Regardless of the actual style used, if you select one of the built-in styles you can access the items through standard properties in the UITableViewCell. Just check the documentation to see which property maps to which part of the cell style. Finally we write tableView:commitEditingStyle: to accommodate the delete edits that will be exposed by the Edit button on the toolbar. This method was commented out in the template, so we need only to uncomment it, add the line to remove the object from our array, and delete the code that handles inserts. If you forget this step the edit button will function, but the app will crash as soon as the user attempts to delete an item. This code is quite similar to an example from the chapter on tables, so it should also be familiar to you. RootViewController.m (section 4 of 4)
#pragma mark - UITableView Data Source / Delegate - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [shoppingList count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; } ShoppingListItem *item = [shoppingList objectAtIndex:indexPath.row]; cell.textLabel.text = item.name; cell.detailTextLabel.text = [NSString stringWithFormat:@"Quantity: %0.2lf", item.quantity]; return cell; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle

Nicolaas tenBroek (AMDG)

236

iOS Development RootViewController.m (section 4 of 4)

In-App Navigation

forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [shoppingList removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } @end

That is all the code we need. Run the app at this point and we have a simple but functional shopping list that also demonstrates navigation. Here is what our shopping list looks like after adding a two items:

Nicolaas tenBroek (AMDG)

237

iOS Development Pressing the add button (+) displays the create item screen:

In-App Navigation

And finally, if we press the Edit button we see the delete controls appear and the Edit button changes to a Done button:

Nicolaas tenBroek (AMDG)

238

iOS Development

In-App Navigation

All in all, this was a simple app to build. Notice though that virtually all the code we wrote was for managing the data. In fact, we needed only two lines of code to make the navigation happen. We think you will agree that it was a pretty good return on the time you invested.

Nicolaas tenBroek (AMDG)

239

iOS Development

In-App Navigation

Navigation Controller With Storyboard


Creating a navigation-based app with the storyboard is not terribly different from what we did in the previous example. Begin by creating a new project using the Single View Application template, ensuring that both Storyboard and ARC are selected. Open the Storyboard in IB and you will see a single view controller window and the arrow indicating the starting point of the app.

We do not need the view that comes with the template, so lets remove it before we try anything else. Select both ViewController.h and ViewController.m, right-click them and choose delete from the context menu. Then choose Delete from the pop-up dialogue. Next select the view controller in the Storyboard and delete it as well. When you delete the view in the Storyboard the arrow pointing to it will disappear as well, but it will be automatically added back in the next step. Select Navigation Controller from the object list:

And drag it onto the design area:

Nicolaas tenBroek (AMDG)

240

iOS Development

In-App Navigation

Next create a new class using the UIViewController subclass template. Just as we did in the previous example we will name this class RootViewController and make it a subclass of UITableViewController, but this time we will not create a nib. Next, select the View Controller associated with the pane titled Root View Controller in the nib and delete it. After the extra view has been removed select a Table View Controller from the object list and drag it onto the designer.

Then use the Identity Inspector to change the new Table View Controllers class to RootViewController.

The next step will identify the table as the root view controller, which we did in the app delegate in the previous example. Right-click on the Navigation Controller and wire its Relationship rootViewController element to the new table view.

Nicolaas tenBroek (AMDG)

241

iOS Development

In-App Navigation

The Storyboard should reflect the relationship with an arrow connecting the two elements:

You should also notice that the view changed itself to include a simulated navigation bar after the relationship was created, which is quite handy. The table itself is almost complete, but if we take a moment to examine the display we will see that tables are represented in a different way when using the Storyboard than they were when using a nib. Specifically, a section at the top is titled Prototype Cells which allows us to design the cells for our table within the table itself (which is quite handy). Select the prototype and in the Attributes Inspector set the style to Right Detail and the Identifier to Cell.

We can now create the edit screen. Create a new class using the UIViewController subclass template, name it CreateListItemViewController and ensure it is subclassing UIViewController (no nib required). Then, in the Storyboard drag a new View Controller object onto the scene and change its type to CreateListItemViewController. This new view needs to be connected via a button on the navigation bar, so select a Bar Button Item from the object list and drag it onto the right side of the tables navigation bar. In the Attributes Inspector change the buttons Identifier to Add. We will need to wire that button to the new view. Right-click the button and you will see a list of Storyboard Segues. Wire the push segue to the new view.

Nicolaas tenBroek (AMDG)

242

iOS Development

In-App Navigation

Note that the new views display changes to include a simulated navigation bar as well, which means we can now add the labels and text fields for the item name and quantity. Set those up the same way we did in the previous example, though we cannot wire them up at this point because we have not written the class. Actually, the classes for CreateListItemViewController and ShoppingListItem will be exactly the same as they were in the previous example, so we will not list the code here. Once you have completed those two classes you can wire-up the outlets and delegates for the create screen. The RootViewController class will be slightly different from the previous example, but should be mostly familiar. RootViewController.h
#import <UIKit/UIKit.h> #import "CreateListItemViewController.h" @interface RootViewController : UITableViewController <CreateItemDelegate> @end

The header file for RootViewController is exactly the same as it was in the previous example and simply indicates that we are implementing the CreateItemDelegate. In the first section of code for the source file we have replaced the createShoppingListItem method with a prepareForSegue:sender: method. Our control links to only one other view, so we are ignoring almost all the data available and using the segue only to access the new view and setup the delegate property. RootViewController.m (section 1 of 4)
#import "RootViewController.h" #import "ShoppingListItem.h" @implementation RootViewController { @private NSMutableArray *shoppingList; } #pragma mark - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { [segue.destinationViewController setDelegate:self];

Nicolaas tenBroek (AMDG)

243

iOS Development RootViewController.m (section 1 of 4)


}

In-App Navigation

The addShoppingListItem: is exactly the same as it was in the previous example. RootViewController.m (section 2 of 4)
#pragma mark - CreateItemDelegate - (void)addShoppingListItem:(ShoppingListItem *)newItem { [shoppingList addObject:newItem]; [self.tableView reloadData]; }

This section has some minor differences from the way it was before. We eliminated the initWithNib:bundle: method and moved the creation of the data model (array) to viewDidLoad. This isnt really important, and was done only to simplify the code. However, you may also notice that the viewDidLoad is missing the lines which created the add button. That step was handled through the Storyboard, so we only need to take care of the edit button here. RootViewController.m (section 3 of 4)
#pragma mark - UIViewController - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.leftBarButtonItem = self.editButtonItem; shoppingList = [[NSMutableArray alloc] init]; }

The only changes in the next section of code are in tableView:cellForRowAtIndexPath:. In fact, all we did in that method was remove the lines which created a cell when the dequeue process returned nil. That step is being handled in the Storyboard, so we have less code to write. RootViewController.m (section 4 of 4)
#pragma mark - UITableView Data Source / Delegate - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [shoppingList count]; }

Nicolaas tenBroek (AMDG)

244

iOS Development RootViewController.m (section 4 of 4)

In-App Navigation

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; ShoppingListItem *item = [shoppingList objectAtIndex:indexPath.row]; cell.textLabel.text = item.name; cell.detailTextLabel.text = [NSString stringWithFormat:@"Quantity: %0.2lf", item.quantity]; } return cell;

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [shoppingList removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } @end

With those code changes in place the app should function exactly the same as the previous example did. All we truly gained from this particular example was more experience working with the Storyboard and a different way to represent an app. Navigation-based apps are designed to be large, so this may be the best use of the Storyboards ability to visually represent the structure and flow of an app.

Nicolaas tenBroek (AMDG)

245

iOS Development

In-App Navigation

Split View / Master-Detail


The Split View or Mater-Detail application is one that uses a navigation view supporting two screens. This setup was designed to take advantage of the additional screen space provided by the iPad to offer a different navigational interface, and has proven to be both an extremely efficient and easily customisable design. While the UISplitViewController is only supported on the iPad (running it on an iPhone will cause the app to crash), in XCode 4.2 the template for the Split-View Based Application indicates that the iPhone is also supported. In fact the template produces two very different apps depending on the device selection. When iPad is selected the app is constructed using the UISplitViewController, and when iPhone is selected the app is built using a table and UINavigationController. To fully understand the possibilities of this mechanism we will develop the example in this section for the iPad and ignore the iPhone settings as they do not offer anything new (other than some confusion). In the Split View the window is actually made up of two different views and a navigation bar. One view is called Master and the other Detail. On the iPad the display of the Master and Detail views can use one of two layouts. In one layout the Master is displayed along the left side of the screen, taking up just under a third of the available screen width while the Detail is displayed in the remaining space. The second layout has the Detail view occupying the entire screen. The Master view is then displayed in a popover dialogue only when it is needed. The navigation bar is used to display titles for both views as well as a button for displaying and hiding the popover version of the Master. The transition between the two layouts is handled automatically which allows for quick development of an app that changes its setup in reaction to interface changes. The traditional layout for the split view is to display both views when the device is in landscape mode and use the popover when the device is in portrait mode. There are several built-in apps on the iPad that utilise this layout, but they are heavily customised making then poor choices for a demonstration. To see it best we can look at screenshots of the app we are about to build. This first image is in portrait mode with the popover displayed:

Nicolaas tenBroek (AMDG)

246

iOS Development

In-App Navigation

And next image is the same app in landscape mode:

The layout and display manipulation is handled through UISplitViewController. There is not much of an interface present in UISplitViewController, it simply extends UIViewController and provides a delegate and an array of exactly two view controllers. The choice of an array would seem to indicate that you are able to create a design with more than two views, but that is not the case. The array must always contain exactly two view controllers. The first controller in the array (index 0) is the Master, and the second is the Detail.

Nicolaas tenBroek (AMDG)

247

iOS Development

In-App Navigation

The delegate protocol provides four methods. One informs you about an impending popover display of the Master. You can use this method to handle any last-second issues before the Master is displayed. Another informs you that the popover display of the Master is about to be dismissed (hiding the Master). The third method informs you when the Master is about to be displayed next to the Detail rather than in a popover. This gives you the opportunity to remove the button from the navigation bar that causes the popover to display. The last method was added in iOS 5 and asks the delegate if the master should be hidden in a particular orientation. In previous versions of the OS the master was always hidden when the interface was in a portrait orientation, and always shown in landscape. In iOS 5 and later, you can change this behaviour to fit your needs. The sparse interface provided by UISplitViewController means that there is not much customisation built in. It also means one critical item not handled by the split view controller: communication between the Master and the Detail controllers. Indeed, you must handle the communication manually, though depending on your design that might be a trivial matter. At its simplest that communication could be only a method call, at its most complex will likely be a delegate situation. Additionally, if you want to change the default behaviour of the split view controller (if for instance, you always want the split displayed, or always want to use a popover), you must subclass UISplitViewController and override the willAnimateToInterfaceOrientation:duration: method. If you were forcing a split screen to always appear then in this method you would need to layout the screens yourself. For our example implementation we will keep the default behaviour. This will give you the opportunity to explore the options that a changing layout can provide. As the previous screen shots demonstrated, in this example we will return to our list of American space missions for a subject, but use the additional screen space to provide a much more interesting application. Begin by creating a new project using the Master-Detail Application template. Select the iPad device family, do not use the Storyboard, but enable ARC.

As we found the last time we created a space-based application, we will need two classes: one for projects, and another for missions. This time however, our classes will be much simpler than they were

Nicolaas tenBroek (AMDG)

248

iOS Development

In-App Navigation

in previous examples which will save us a great deal of typing. Our new Project class contains only an array of missions and a project name. Project.h
#import <Foundation/Foundation.h> @interface Project : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) NSMutableArray *missions; - (id)initWithName:(NSString *)projectName andMissions:(NSMutableArray *)projectMissions; @end

Project.m
#import "Project.h" @implementation Project { @private NSString *name; NSMutableArray *missions; } @synthesize name; @synthesize missions; - (id)initWithName:(NSString *)projectName andMissions:(NSMutableArray *)projectMissions { if(self = [super init]) { name = projectName; missions = projectMissions; } return self; } @end

Our Mission class is also a great deal smaller and less complex than it was before. We now need only a mission name and a URL for finding the mission on the web. Mission.h
#import <Foundation/Foundation.h> @interface Mission : NSObject @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) NSString *url;

Nicolaas tenBroek (AMDG)

249

iOS Development Mission.h


- (id)initWithMissionName:(NSString *)missionName andURL:(NSString *)missionURL; @end

In-App Navigation

Mission.m
#import "Mission.h" @implementation Mission { @private NSString *name; NSString *url; } @synthesize name; @synthesize url; - (id)initWithMissionName:(NSString *)missionName andURL:(NSString *)missionURL { if(self = [super init]) { name = missionName; url = missionURL; } return self; } @end

Now that our very simple data classes are complete we can move on to the App Delegate. With the release of XCode 4.2 Apple made some rather large changes to the application template and not all of them were good. In fact, as delivered this template is quite broken. If we were to leave the App Delegate untouched the entire detail section of the screen would forever go untouched, while all the operation took place within the section occupied by the master. Thankfully this situation is easy enough to fix, and hopefully Apple will repair the template soon. For now we will need to engage in a little repair work. (If you wish to see the problem, simply run the app now and try selecting the demonstration item in the table.) The fix for the App Delegate is quite simple; we need only to populate the masters detailViewController reference with the address of the actual instance of DetailViewCOntroller. That line is in bold below, but before we move on we should take a careful look at the construction. In the previous versions of this template the bar on the top of the screen was simply a UIToolbar and virtually all the setup was handled in IB. In this version the toolbar has been replaced with the UINavigationBar which we received by creating two separate UINavigationController instances one for each view. To the author this seems more than a bit heavy-handed given everything the navigation controller does. Had the code authors not used the navigation controllers, they would not have been able to make the mistakes they did in this template, which makes it seem worse than even a resource drain, it is dangerous as well. It would be

Nicolaas tenBroek (AMDG)

250

iOS Development

In-App Navigation

well worth a few minutes of your time to replace the navigation controllers with toolbars for a production app, but we will leave them in place for this example. AppDelegate.m
#import "AppDelegate.h" #import "MasterViewController.h" #import "DetailViewController.h" @implementation AppDelegate @synthesize window = _window; @synthesize splitViewController = _splitViewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. MasterViewController *masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController" bundle:nil]; UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController]; DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil]; masterViewController.detailViewController = detailViewController; UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController]; self.splitViewController = [[UISplitViewController alloc] init]; self.splitViewController.delegate = detailViewController; self.splitViewController.viewControllers = [NSArray arrayWithObjects:masterNavigationController, detailNavigationController, nil]; self.window.rootViewController = self.splitViewController; [self.window makeKeyAndVisible]; return YES; } @end

With the App Delegate repaired we can turn our attention to the Master, which will also need some repair. Before we see the broken section though, notice that the Split View template provides a UITableViewController as the master. In fact, looking at the code you can tell it is virtually the same template used to create a UITableViewController. This class template will work out well for us because we needed a table to display the missions.

Nicolaas tenBroek (AMDG)

251

iOS Development MasterViewController.h


#import <UIKit/UIKit.h> @class DetailViewController; @interface MasterViewController : UITableViewController @property (strong, nonatomic) DetailViewController *detailViewController; @end

In-App Navigation

Here we see the property defined which we populated in the App Delegate. This property provides the mechanism we will use to communicate about the users selection from the list. There is quite a bit of code in MasterViewController.m, but most of it you have seen before. We will begin our examination with initWithCoder:. Just as in previous examples the vast majority of the code in this method is setting up the data for our model. However, there are a few other items of interest, and those have been placed at the top of the method for easier access. We begin by setting the title property. Recall that the navigation controller will pick up this property and use it to set the title label of the navigation bar. Next, we turn off the automated row selection clearing. By default tables clear all selections in viewDidAppear: working from the assumption that when this method is called the screen needs to be fresh. This time however, the table may be displayed in a popover (depending on the devices position) and might be repeatedly added to the screen and removed from it without changing its data. We want to save the selection data from one appearance to the next so that the user always knows what is happening. Next we set the desired size of the popover dialogue. That size is hard-coded for now, but might need to be changed to a calculation if Apple adds devices with different screen sizes in the future. MasterViewController.m (section 1 of 4)
#import #import #import #import "MasterViewController.h" "DetailViewController.h" "Project.h" "Mission.h"

@implementation MasterViewController { @private NSMutableArray *projects; } @synthesize detailViewController = _detailViewController; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { self.title = NSLocalizedString(@"Missions", @"Missions"); self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0); projects = [[NSMutableArray alloc] init];

Nicolaas tenBroek (AMDG)

252

iOS Development MasterViewController.m (section 1 of 4)

In-App Navigation

//"Project Mercury." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Mercury>. Mission *mercury1 = [[Mission alloc] initWithMissionName:@"Little Joe 1" andURL:@"http://en.wikipedia.org/wiki/Little_Joe_1"]; Mission *mercury2 = [[Mission alloc] initWithMissionName:@"Big Joe 1" andURL:@"http://en.wikipedia.org/wiki/Big_Joe_1"]; Mission *mercury3 = [[Mission alloc] initWithMissionName:@"Little Joe 6" andURL:@"http://en.wikipedia.org/wiki/Little_Joe_6"]; Mission *mercury4 = [[Mission alloc] initWithMissionName:@"Little Joe 1A" andURL:@"http://en.wikipedia.org/wiki/Little_Joe_1A"]; Mission *mercury5 = [[Mission alloc] initWithMissionName:@"Little Joe 2" andURL:@"http://en.wikipedia.org/wiki/Little_Joe_2"]; Project *mercury = [[Project alloc] initWithName:@"Mercury" andMissions: [NSMutableArray arrayWithObjects:mercury1, mercury2, mercury3, mercury4, mercury5, nil]]; [projects addObject:mercury]; //"Project Gemini." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Gemini>. Mission *gemini1 = [[Mission alloc] initWithMissionName:@"Gemini 1" andURL:@"http://en.wikipedia.org/wiki/Gemini_1"]; Mission *gemini2 = [[Mission alloc] initWithMissionName:@"Gemini 2" andURL:@"http://en.wikipedia.org/wiki/Gemini_2"]; Mission *gemini3 = [[Mission alloc] initWithMissionName:@"Gemini III" andURL:@"http://en.wikipedia.org/wiki/Gemini_3"]; Mission *gemini4 = [[Mission alloc] initWithMissionName:@"Gemini IV" andURL:@"http://en.wikipedia.org/wiki/Gemini_4"]; Mission *gemini5 = [[Mission alloc] initWithMissionName:@"Gemini V" andURL:@"http://en.wikipedia.org/wiki/Gemini_5"]; Project *gemini = [[Project alloc] initWithName:@"Gemini" andMissions: [NSMutableArray arrayWithObjects:gemini1, gemini2, gemini3, gemini4, gemini5, nil]]; [projects addObject:gemini]; //"Project Apollo." Wikipedia. Web. 29 November 2010. //<http://en.wikipedia.org/wiki/Project_Apollo>. Mission *apollo1 = [[Mission alloc] initWithMissionName:@"AS-201" andURL:@"http://en.wikipedia.org/wiki/AS-201"]; Mission *apollo2 = [[Mission alloc] initWithMissionName:@"AS-203" andURL:@"http://en.wikipedia.org/wiki/AS-203"]; Mission *apollo3 = [[Mission alloc] initWithMissionName:@"AS-202" andURL:@"http://en.wikipedia.org/wiki/AS-202"]; Mission *apollo4 = [[Mission alloc] initWithMissionName:@"AS-204" andURL:@"http://en.wikipedia.org/wiki/Apollo_1"]; Mission *apollo5 = [[Mission alloc] initWithMissionName:@"Apollo 4" andURL:@"http://en.wikipedia.org/wiki/Apollo_4"]; Project *apollo = [[Project alloc] initWithName:@"Apollo" andMissions: [NSMutableArray arrayWithObjects:apollo1, apollo2, apollo3, apollo4, apollo5, nil]]; [projects addObject:apollo]; }

Nicolaas tenBroek (AMDG)

253

iOS Development MasterViewController.m (section 1 of 4)


return self; }

In-App Navigation

In contrast to initWithCoder:, viewDidLoad is very short but quite interesting. The template provides a line of code which automatically selects the first row in the table. In the template there is no cost to this selection, but in our project selecting a mission will load a web page. Auto-selecting the first row forces the user to endure the loading of its associated web page, whether or not they were interested in it. Obviously it is best to leave that decision to the user, so we have commented the line out. You can delete it rather than commenting it out as you do not need it at all. In the method shouldAutorotateToInterfaceOrientation: we simply return YES regardless of the orientation as we are supporting them all. If there are some orientations you do not want to support, then you would need to examine the argument and return YES or NO based on its value. For instance, you might decide to support only portrait modes, or only landscape modes. If you make such a restriction, make sure you support both versions of the mode you choose. For instance, if supporting only the landscape orientation, then you should make sure you support both landscape left and landscape right. If supporting only portrait, support both right side up and upside down. There is no reason to force support for only one side of a mode, and users are often frustrated by apps that choose to support only one. MasterViewController.m (section 2 of 4)
#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // [self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; }

The table data source and delegate section is pretty standard code, which you have seen before. In fact, the only part that is in any way different from what we have seen before is in tableView:didSelectRowAtIndexPath:, so we will treat it separately below. MasterViewController.m (section 3 of 4)
#pragma mark - UITableView DataSource / Delegate - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [projects count]; }

Nicolaas tenBroek (AMDG)

254

iOS Development MasterViewController.m (section 3 of 4)

In-App Navigation

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [[projects objectAtIndex:section] name]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[projects objectAtIndex:section] missions] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell. cell.textLabel.text = [[[[projects objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row] name]; return cell; }

In the last section we have tableView:didSelectRowAtIndexPath:. You might recall that in our previous space-related app we displayed an alert view when a row was selected. In this app we instead inform the Detail that it needs to display the data at the URL for the mission. Before we get to that single line of code, we will need to do some repair work. This method was designed to work with the broken App Delegate and is the main cause of the functionality failure. The provided code creates a new DetailVewiController, and then uses the navigation controller to push the new view. This approach is very wrong for a split-view and needs to be removed entirely. We have commented the offending code for the readers sake, but it should simply be deleted and replaced with the proper line below. MasterViewController.m (section 4 of 4)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // if (!self.detailViewController) { // self.detailViewController = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil]; // } // [self.navigationController pushViewController:self.detailViewController animated:YES]; [self.detailViewController displayURL:[[[[projects objectAtIndex:indexPath.section] missions] objectAtIndex:indexPath.row] url]]; } @end

Nicolaas tenBroek (AMDG)

255

iOS Development

In-App Navigation

Next, we can take a look at the DetailViewController. The template builds this controller with a label and an id called detailItem. We do not need either of these, so we can delete them. We will be adding a web view and a method prototype called displayURL:. This is the method that was called in MasterViewController when a row was selected in the table. Finally, we need to implement the UIWebViewDelegate. We will use this to hide an activity indicator when the web page has finished loading, and to update the views title. DetailViewController.h
#import <UIKit/UIKit.h> @interface DetailViewController : UIViewController <UISplitViewControllerDelegate, UIWebViewDelegate> @property (nonatomic, weak) IBOutlet UIWebView *webView; - (void)displayURL:(NSString *)url; @end

The template for DetailViewController.m provides most of the code we need, so there will not be too many changes here. Nonetheless, it will be worth the time to examine the code. The code begins by creating a private category to hold a property and setup method. The setup method is not needed, and frankly was poorly conceived. We will delete the entire category, though we will replace the property with an instance variable. We will actually need two instance variables: one is the UIPopoverController which we moved from the category, but the other is a UIActivityIndicatoryView which we will use to inform the user when work is happening. If you redesign this app to use a toolbar in place of the navigation controller, then you can move the activity indicator to the nib and create a property. For now we will handle it all in code. The displayURL: method is pretty straightforward; first we start the activity indicator spinning, change the views title to Loading and then ask the web view to load the requested URL. Finally we check to see if the popover controller exists, and hide it if it does. Remember that in landscape mode the popover will not have been displayed. DetailViewController.m (section 1 of 4)
#import "DetailViewController.h" @implementation DetailViewController { @private UIActivityIndicatorView *activityIndicator; UIPopoverController *masterPopoverController; } @synthesize webView; #pragma mark - Managing the detail item

Nicolaas tenBroek (AMDG)

256

iOS Development DetailViewController.m (section 1 of 4)

In-App Navigation

- (void)displayURL:(NSString *)url { [activityIndicator startAnimating]; self.title = NSLocalizedString(@"Loading...", @"Loading..."); [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]]; if (masterPopoverController != nil) { [masterPopoverController dismissPopoverAnimated:YES]; }

Next we can write our web view delegate methods. The delegate actually provides notification of four different events: the loading started, the loading finished, the loading failed with an error, and another which allows you to cancel the request before it starts to load. We have only implemented the finish loading and failure methods. In webViewDidFinishLoad: we set the views title and stop the activity indicator from spinning. Getting the title is an interesting effort. The web view does not provide many methods or properties to give you information about the page that was loaded. However, you can inject and run JavaScript on the page, which enables you to do just about anything you wish to the HTML. In this example we use a short piece of script to retrieve the pages title. Once we have the title we place it in our title property. There really isnt much we can do when the web page fails to load, but doing nothing is a pretty poor approach. Here we are doing the very least we can do, which is informing the user that the error occurred. A better approach would be to examine the cause of the error, and do some further investigation. For instance, we may be in airplane mode, which means the network has been disabled, or we might not have a valid network connection. Telling the user what is actually wrong will make it easier for them to fix, but we will leave that for a later chapter. DetailViewController.m (section 2 of 4)
#pragma mark - UIWebViewDelegate - (void)webViewDidFinishLoad:(UIWebView *)aWebView { self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"]; [activityIndicator stopAnimating]; } - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[error localizedFailureReason] message:[error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }

We do not have to do much with the standard view controller methods in this class, but we do need a little bit of setup and to enable support for autorotation. You must ensure that whatever orientations you supported in MasterViewController are also supported in DetailViewController. Thankfully, our

Nicolaas tenBroek (AMDG)

257

iOS Development

In-App Navigation

example is supporting everything, so thats a pretty easy requirement to fulfil. The viewDidLoad method appears a bit more complicated, and all the complication is arising due to the use of the navigation controller. If you use a toolbar then all but one line would be handled with a few mouse clicks in IB. The first line simply sets the title to No Mission Selected which is about as informative as we can be. The remaining code creates a UIActivityIndicatorView and positions it ten pixels from the right side of the screen and in the centre of the navigation bar. We also set its auto resizing mask to use a flexible left margin. The effect of a flexible left margin is that the control will stay in the same position relative to the right side of the screen. It can be a bit hard to see the results of this because the activity indicator will be on the screen for very short periods of time. If you want to see the indicator while designing the app simply set its hidesWhenStopped property to NO. That will force it to show at all times and can help you verify the placement. Dont forget to remove that line before heading to production! DetailViewController.m (section 3 of 4)
#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.title = NSLocalizedString(@"No Mission Selected", @"No Mission Selected"); //create activity indicator so user knows when page is loading activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [self.navigationController.navigationBar addSubview:activityIndicator]; //position activity indicator on right side of nav bar CGRect boundingRect = self.navigationController.navigationBar.frame; CGRect activityRect = activityIndicator.frame; activityRect.origin.x = boundingRect.size.width - activityRect.size.width - 10; activityRect.origin.y = (boundingRect.size.height - activityRect.size.height) / 2; activityIndicator.frame = activityRect; //keep position relative to the right margin when the view is reshaped activityIndicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; }

In the final section of code we will examine the UISplitViewControllerDelegate methods. The method splitViewController:willHideViewController:withBarButtonItem:forPopoverController: is called when the popover is hiding. We will need to set the passed-in buttons title to Missions and then add the button to the navigation controller. We also store a reference to the master which will be used when we are ready to hide the master in displayURL:. The companion to this method is splitViewController:willShowViewController:invalidatingBarButtonItem:. We do not need to change

Nicolaas tenBroek (AMDG)

258

iOS Development

In-App Navigation

that method as it simply removes the buttons from the navigation bar and ensures our reference to the master is nil. DetailViewController.m (section 4 of 4)
#pragma mark - Split view - (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController { barButtonItem.title = NSLocalizedString(@"Missions", @"Missions"); [self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES]; masterPopoverController = popoverController; } - (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { //Called when the view is shown again in the split view, //invalidating the button and popover controller. [self.navigationItem setLeftBarButtonItem:nil animated:YES]; masterPopoverController = nil; } @end

At this point we are ready to move into the Interface Builder to add the new controls, when that step is done we will have a complete app. Open DetailView.xib, and you will see a mostly empty screen with a label in the middle.

Nicolaas tenBroek (AMDG)

259

iOS Development

In-App Navigation

We do not need this label, so drag it off the screen or delete it. Next, turn on the simulated navigation bar, and drag a UIWebView onto the centre section. Depending on where you drop it the UIWebView may or may not resize itself properly, so you might need to drag the edges to fill the content area. Next, open the Attributes Inspector and set the scaling to Scales Page To Fit. You can turn on the other options if you wish.

Next we need to switch to the Size Inspector. The size and shape of the screen will change as the device is rotated, and we need our controls to respond appropriately to those changes. For the web view, turn on all the auto-sizing springs and struts. The struts are the I-Beam shaped controls and tell the control to maintain its relative distance from the edges of the screen on a specific side. The springs are the double-sided arrows and tell the control to stretch or shrink along a specific axis. The red and white rectangle example shows you the results of your auto-sizing decisions, so check that whenever you are not sure what a setting will do.

Now, wire up the web view outlet and delegate property. That is it! Save and run and we have a fully functional split screen application that responds to screen rotations by modifying its layout and communicates between the Master and Detail. In this chapter we have seen several ways to handle navigation within a multi-screened application. Obviously you are not restricted to using only the Utility, Tab Bar, Navigation Controller, and Split View, but you should consider those before deciding to create your own navigation. The pre-built navigation templates have two distinct advantages. First, they already work, and that is something that is not to be undervalued. Second, users are already familiar with them, so you dont have the user-training factor to worry about. If none of those seem a natural fit, then definitely create one of your own. It would be far better to invest the time creating something from whole cloth than to force-fit something that is unnatural.

Nicolaas tenBroek (AMDG)

260

iOS Development

In-App Navigation

Nicolaas tenBroek (AMDG)

261

iOS Development

I/O

I/O
The topic of I/O is a bit more complicated when dealing with mobile platforms than it is when dealing with the standard desktop model. We already know that resources like memory, processing power, and screen-size are more restrictive on a mobile platform, but you may not have considered that storage is also much more restricted. For instance, you may well have individual files stored on your desktop than are larger than the entire storage space on your mobile device. At the time of this writing desktop storage costs less than $0.05 per gigabyte, which means your storage strategy for a desktop application is almost always write a file. Caching data for potential later use is nearly a non-issue from a development standpoint, it is simply expected behaviour. Such caching is usually done to improve application performance and minimise network traffic, both of which are serious issues on mobile platforms as well. On the desktop though, because storage space is so plentiful cache sizes can be quite large and virtually all data is cached. On the mobile device you must be much more cautious about caching because the space is much more precious. Of course, network bandwidth is also more precious on the mobile device, so the need to cache data is actually greater. This puts mobile app developers in a bit of a bind, caching data is far from a luxury, but the ability to do so is extremely restricted. Storage decisions go well beyond simply what to cache though. On a typical desktop there are multiple levels of I/O buffering in place that are combined with dedicated processors and data busses to make I/O as fast as possible. For instance, hard drives contain their own processors and buffers; those are then linked to the I/O subsystem of the motherboard, which also has its own processors and buffers. Those features simply do not exist on mobile devices, or are so comparatively simplistic as to be almost unrecognisable. The end result of this is that I/O barely affects desktop application performance, but has a significant impact on the performance of an entire mobile device. All of those factors mean you must be far more careful about how you implement I/O in a mobile app than you might be in a desktop application. Mobile I/O is also a bit less stable than in the desktop environment. Apps can be paused or shut down at any moment by the user, the OS, or the hardware with little to no warning given to the app. Network connections appear and disappear constantly, and apps need to be able to handle those changes gracefully. For instance, if a user is browsing a web page while travelling, you cannot simply restart the page download every time the network status changes. Doing so could result in a near endless series of restarts. If your app is streaming media, then it must be vigilant about monitoring the network conditions, informing the user of status changes, and recreating connections as necessary without reinvolving the user if at all possible. Caching network data is not the only time you will need to use I/O of course. For instance, you very likely will want to give users the ability to save preferences in your app. Most mobile apps also keep track of what the user is doing so that they can pick up right where they left off if the user has closed the app to do something else. Desktop applications often perform very similar actions, but do so in different ways. For instance, a desktop app may open in its default state and use a menu system to allow you to open up a previous file. That menu will then often open a dialogue that exposes the entire storage system and users can select whatever files they wish from whichever location strikes their fancy. Such a

Nicolaas tenBroek (AMDG)

262

iOS Development

I/O

strategy simply will not work on mobile devices. As we already know, the users controls are much more limited, meaning desktop designs simply do not work well in the mobile environment. Forcing users to browse around looking for files is a guaranteed way to annoy them. The limits go beyond simple screen and interaction limits though. Most modern mobile systems (including iOS) sandbox apps so that the app can only access its own little part of the file system. This prevents them from accessing files from other apps without explicit permission to do so. The ability to backup data is also limited. In a typical desktop application scenario, if the user chooses to uninstall the application they can decide whether or not to keep their settings, and any saved files. Applications that allow the user to generate unique work (i.e. documents, images, presentations, usw.) skip this decision altogether. The uninstall process simply does not involve the user files at all. In contrast, when the user chooses to remove an app from iOS, its entire file system is simply deleted; including the entirety of the users saved work. Of course, with iOS, the app and its data are backed up in iTunes every time a device is synced, but one the app is removed, the backup is as well (at the next sync). This means apps that allow the user to generate unique work must provide some other mechanism for safely storing data (e.g. storing the data in a cloud, or networked device). Finally, on a desktop I/O can be done to virtually any device, while mobile devices have extremely limited sets of devices they are capable of reaching. All of these factors must be considered when planning I/O for a mobile device application.

Nicolaas tenBroek (AMDG)

263

iOS Development

I/O

iOS Application File System


As we mentioned earlier, each app on iOS is sandboxed, and is in effect given its own little file system. The app has both read and write access to most of the system, though there are some restricted areas with read access only. The file system as delivered is actually quite structured, and the structure is relied on for several different purposes, so learning where files should go is quite important. The root of this system contains four directories: Documents, Library, <App-Name>.app, and tmp. We will discuss each of these in turn. Documents The term document has become fairly generic in computing circles and has come to mean: any work generated by the user. The Documents directory is backed up by iTunes each time the device is synced, and is the most appropriate place to store user work. By default this directory is empty, and the app has read/write access. You can create additional directories under this one as needed, and store files anywhere within that directory tree. Beginning in iOS version 3.2 apps were given the ability to expose the documents directory through iTunes so that users could add and remove files, or simply create an additional backup of their work. This document sharing is off by default but can be enabled simply by adding the key UIFileSharingEnabled to the apps Info.plist file. If you choose to enable sharing, then you must ensure files that are produced by your app but do not contain user generated content are stored somewhere other than the Documents directory. The next most appropriate place to store files is in the Library directory. Library This directory is for files that are not user-generated content. By default this directory contains two sub directories: Preferences and Caches. The Preferences subdirectory is where application and user preferences are stored in the form of .plist files. Preferences are discussed in the chapter Preferences and Settings. For now it will suffice to say you should not modify this directory directly. The Caches subdirectory can be used to store any application data that you want to persist between executions of your application, but are not critical to the operation of the application itself. Any data stored here should be easy to recreate, as this folder is not backed up by iTunes. Its data will persist across application updates, but will not be preserved if the device is restored from a backup. This is a good place to store state type information to enable the application to pick up where it left off on the last run. You are free to create your own subdirectories of Library, as long as their names do not clash with the existing subdirectories. In fact, you cannot write files directly in the Library directory. Only subdirectories may be added. So, if you need to store application private data and have enabled document sharing, you should then create a subdirectory of Library for that purpose. All additional subdirectories of Library will be backed up by iTunes.

Nicolaas tenBroek (AMDG)

264

iOS Development

I/O

<App-Name>.app The real name of this directory changes with each app. The <App-Name> portion is replaced with the name of your app. So, if we wrote an app named iOSExplorer, then the real name of this directory would be iOSExplorer.app. This directory is the application bundle, containing the application and all of its resources (nib files, images, sounds, and anything else you place in the Resources folder in XCode). While you are free to read from this folder, it must never be written to. In fact, the bundle is signed when the application is installed on the device, and any modification (other than installing an update) will prevent the app from launching. When you refer to a nib file or image name in code, it is automatically loaded from this directory. tmp As its name (almost) indicates, this is the temp folder and should be used the same way the temporary folder is used in a desktop application. Any data that is non-critical and transitory should be stored here. There is no guarantee that files stored here will persist across application launches. You should take care to delete anything stored in this directory as soon as it is no longer needed. Even if you do not actively delete files from this directory, the OS may well (and often does) delete any files stored here if the application is not running. Obviously this directory is not backed up by iTunes and its children will not persist across application updates.

Nicolaas tenBroek (AMDG)

265

iOS Development

I/O

Classes Supporting I/O


As file I/O is fairly restricted in iOS, we simply do not have a need for a great diversity of file I/O options. That means the library classes can accommodate the vast majority of your I/O needs. This also means that enabling I/O within your app can be done very quickly after learning the library. NSFileManager The vast majority of your file and directory management needs can be handled through the class NSFileManager. This class contains methods for creating, deleting, moving, comparing, interrogating, and most any other operation you can think of. Despite its name, most of the methods in this class function on both files and directories. It should be noted that in older versions of iOS the use of the class method defaultManager was encouraged for getting an instance of NSFileManager. However, this singleton instance is not thread-safe, and should be avoided. Instead you are encouraged to alloc and init your own instance to ensure safety in a multi-threaded environment. It will be well worth your time to take four or five minutes to familiarise yourself with the documentation for this class. NSPathUtilities NSPathUtilities isnt actually a class; instead it is a category of NSString and adds a large number of class methods for handling file and directory paths. The methods provided by this category are the most appropriate mechanism for constructing or deconstructing paths to resources. Manual construction of paths should be avoided, as it is error prone and will not handle changes to the underlying file system. If the file system is modified in future versions of iOS, then apps that use the path methods will continue to work. If you manually build your paths then you must revalidate them after each iteration of the OS. NSFileHandle The class NSFileHandle provides a convenient interface abstraction for handling files that could be either local to the device or opened through a network connection. It contains class methods for creating file handles that can be read from or written to, but not both. The most basic I/O methods (read and write) are provided, but there is no control over how the I/O will be handled. This extreme abstraction and simplicity unfortunately means you cannot tune the performance of your application when carrying out I/O operations. NSStream NSStream is a much more powerful mechanism for file I/O than NSFileHandle, but it is also more complicated. NSStream itself is abstract, so you will instead directly use its children: NSInputStream and NSOutputStream. NSStream provides methods for opening and closing streams, interrogating the streams status and error conditions, and most importantly, for scheduling I/O which provides support for threaded I/O processing. The subclasses provide methods for reading or writing with the ability to control the amount of data transferred with each operation. This control is critical for ensuring that your application remains responsive during I/O operations.

Nicolaas tenBroek (AMDG)

266

iOS Development

I/O

Data Classes
Some of the major classes you will use to collect data actually support I/O on their own. The classes NSString, NSArray, NSDictionary, and NSData each add methods called wrtiteToFile: and writeToURL: as well as initWithContentsOfFile: and initWithContentsOfURL:. Each class adds additional arguments to the methods as appropriate to the data type, but they all start with the same names. While the methods provided by these classes are very easy to use, they are best avoided in larger I/O operations. The methods do not provide the ability to schedule the operations to take place in another thread, nor do they provide any control over the number of bytes read or written at a time. When dealing with I/O operations around a few hundred bytes (i.e. reading a preferences file or application state), they will work fine, but for larger files these methods should be avoided. An additional restriction is applied to NSArray and NSDictionary. In order to use the I/O methods the contents of the array or dictionary must be one of the classes that can be used in a property list (NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary). Property Lists will be covered in the chapter on Preferences and Settings.

Nicolaas tenBroek (AMDG)

267

iOS Development

I/O

Examples
Rather than building a giant Do it all type example demonstrating all the I/O possibilities, we will construct several smaller ones, which will allow us to focus on specific parts of the I/O libraries. Paths and Directories Lets begin by creating an iOSExplorer to give us a view of the apps file system. Considering that the file system is a tree, a Navigation-based Application appears the most appropriate choice. Create one using the Empty Application template and name it iOSExplorer. The singular purpose of this app will be the display of the file system, so it will be very simple. In fact, we will need only one view, and the provided template code will require very small changes. Add a new UITableViewController subclass named RootViewController, and ensure we also add a nib. We can make good use of RootViewController by making it generic enough to display the contents of any directory. Then, we can simply use new instances of it for each directory we want to display. RootViewController needs to know the name of the directory it is displaying, but that will be the only property. Nothing else need be exposed to outside classes. RootViewController.h
#import <UIKit/UIKit.h> @interface RootViewController : UITableViewController @property (nonatomic, strong) NSString *directory; @end

Every table requires a data model for our table, and as it often does an array will suffice for this one. We will require one array for the directory contents, and add an additional for cell accessories which will serve to differentiate files from directories in the display. We need to write our own setter method for the directory property, because when the directory is set we will need to build the array of directory contents. So, in setDirectory: we carry out the standard assignment and then setup the rest of the data and the view. We set the view controllers title using one of the NSPathUtilities methods called lastPathComponent, which simply parses the path and returns the last element represented by the string. The last element will either be a file name or a directory name, and in this case, the element is the name of the current directory. Finally, we call the method readDirectoryContents. In readDirectoryContents we create an instance of NSFileManager, and ask it for the contents of the directory. Of course, most any I/O operation can fail, so we need to check whether or not our operation was successful. If our read was successful then we need to save the array returned. If the read failed for some reason, then we notify the user about the failure with an alert. In order to make our table display a bit more informative, we will add a disclosure indicator to each cell that represents a directory. (You can see that particular line of code later when we are setting up the Nicolaas tenBroek (AMDG) 268

iOS Development

I/O

cell in tableView:cellForRowAtIndexPath:.) When a disclosure indicator is present the user will know there is something more to see if they tap the cell. Of course, in order to know when to add the disclosure indicator we need to know if each element is representing a file or a directory. The only way to find that information is to ask the File Manager. We could put that question off until we are building the cell itself, but if we had a large array the constant checks would definitely slow down the user interface. So we made the decision to check each element before the table is built, and store the results in another array. In a startling demonstration of poor design decisions, NSFileManager does not have a method which specifically tests whether or not a given path represents a file or directory. In order to discover this critical piece of information we are forced to use the fileExistsAtPath:isDirectory: method. As the primary purpose of this method is to test the existence of a file or directory, the return type has already been used. So, we are forced to pass in the address of a BOOL variable to get the data we really need. In order to make the test, the file manager needs a fully qualified path name, but the values returned from contentsOfDirectoryAtPath: are only the last path components. Therefore we must reconstruct the path for each element using another NSPathUtilities method called stringByAppendingPathComponent:. Once we have discovered whether or not each element is a directory, we can assign the appropriate accessory (detail disclosure or none) to the array. Finally, we inform the table that the data model has changed and the table needs to be reloaded. RootViewController.m (section 1 of 2)
@interface RootViewController () - (void)readDirectoryContents; @end @implementation RootViewController { @private NSString *directory; NSArray *contents; NSMutableArray *accessories; } @synthesize directory; #pragma mark - Custom Accessors - (void)setDirectory:(NSString *)directoryPath { directory = directoryPath; self.title = [directory lastPathComponent]; [self readDirectoryContents]; } #pragma mark - private methods - (void)readDirectoryContents { NSFileManager *fileManager = [[NSFileManager alloc] init];

Nicolaas tenBroek (AMDG)

269

iOS Development RootViewController.m (section 1 of 2)


NSError *error = nil; contents = [fileManager contentsOfDirectoryAtPath:directory error:&error]; if(error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error Reading Directory" message:[NSString stringWithFormat:@"The contents of %@ could not be read.", [directory lastPathComponent]] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; return; } accessories = [[NSMutableArray alloc] initWithCapacity:[contents count]]; BOOL isDirectory = NO; for(NSString *path in contents) { [fileManager fileExistsAtPath:[directory stringByAppendingPathComponent:path] isDirectory:&isDirectory]; [accessories addObject:[NSNumber numberWithInt:isDirectory? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone]]; } [self.tableView reloadData]; }

I/O

Next we have the standard table data source and delegate methods. You have seen these several times now, so we will not cover them in detail. You should take a closer look at tableView:didSelectRowAtIndexPath: though. When a row is selected we need to decide whether the row represents a file or a directory. We can make that decision by examining the accessory at the selected index. If the selected cell does have an accessory we know it represents a directory. In that case we create a new instance of the root view controller, pass it a reconstructed full path to the selected directory, and then ask the navigation controller to display the new view.

RootViewController.m (section 2 of 2)
#pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [contents count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell";

Nicolaas tenBroek (AMDG)

270

iOS Development RootViewController.m (section 2 of 2)


UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } cell.textLabel.text = [[contents objectAtIndex:indexPath.row] lastPathComponent]; cell.accessoryType = [[accessories objectAtIndex:indexPath.row] intValue]; return cell; } #pragma mark - Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if([[accessories objectAtIndex:indexPath.row] intValue] == UITableViewCellAccessoryDisclosureIndicator) { RootViewController *viewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil]; viewController.directory = [directory stringByAppendingPathComponent:[contents objectAtIndex:indexPath.row]]; [self.navigationController pushViewController:viewController animated:YES]; } } @end

I/O

Those changes are all that we need for the root view controller. The one job we have yet to do at this point is to ensure the initial root view displays the root of the apps file system. To implement the initial setup we will need to modify the app delegate. We only added a few lines to the app delegate (they are in bold below), and what we did add should be familiar to you as it almost exactly the same process we used in the shopping list example. First we imported RootViewController.h, and then added instance variables to hold the root view and navigation controller. In application:didFinishLaunchingWithOptions: we created the root view and navigation controller, setting them up in the same manner as our previous navigation example. In fact, the only difference here is that we set the directory property to the apps home using a call to a C-style function called NSHomeDirectory(). That function returns a reference to the root of the current apps file system, so it makes a perfect place to begin our browsing.

Nicolaas tenBroek (AMDG)

271

iOS Development AppDelegate.m


#import "AppDelegate.h" #import "RootViewController.h" @implementation AppDelegate { @private RootViewController *rootViewController; UINavigationController *navigationController; } @synthesize window = _window; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.window.backgroundColor = [UIColor whiteColor]; rootViewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil]; rootViewController.directory = NSHomeDirectory(); navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; [self.window addSubview:navigationController.view]; [self.window makeKeyAndVisible]; return YES;

I/O

} @end

With those changes made our app is complete and we can browse around and see the contents of the file system.

Nicolaas tenBroek (AMDG)

272

iOS Development Simple File I/O For our first file I/O example we will use the methods of NSString to read and write a file in the Documents directory. We need a new app for this example, so create a new Single View Application called SimpleFile. You will need to add a button for saving the file and a UITextView for handling the files contents to the View. All the coding will take place in SimpleFileViewController, though you will find there may be more code to deal with errors than actual I/O work at this point. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (nonatomic, weak) IBOutlet UITextView *textView; - (IBAction)save:(id)sender; @end

I/O

With two quite small methods we are able to produce an app that reads and writes a simple file. The most complicated of those methods is probably save:, so lets examine that one first. This method is obviously called by our save button, which passes itself in as the sender argument. The very first thing we need to do is disable the button to prevent it from being hit again if our save takes very long. For short files this may seem quite silly, but it will be well worth the extra effort later to prevent crashes. This is a practice you should regularly engage in when working with I/O. We want to ensure the users cannot accidentally kill their own files. With the button disabled we can turn our attention to actually saving the file. Here we are using the NSString method writeToFile:atomically:encoding:error:. While the first and last arguments are obvious, the middle ones are not. Despite the name the second argument is actually more about ensuring the write is successful than it is about the discreteness of the operation. If the value of atomically is YES, then the data will actually be written to a temp file. Once the file has been successfully written the original file is deleted and the temp file is renamed to the original file name. This ensures that if an error occurs while writing the file, the original file is not destroyed. If the value of atomically is NO, then the original file is overwritten directly, and any write errors will destroy the file. The encoding argument specifies which character set was used in the construction of the file. As we are writing and reading the file entirely within our app (rather than getting it from another system or sending it to another system), we can use any encoding system we would like. Using UNICODE ensures that any of the standard international characters will be supported. Obviously if you are interacting with other systems, you would need to use the encoding system agreed to by the developers of that system. NSString supports a large number of encodings, and they are documented in the constants section of the NSString documentation.

Nicolaas tenBroek (AMDG)

273

iOS Development

I/O

That single method call takes care of all the file output for us, so there is not much left to do. Of course, virtually every I/O operation can cause errors, so always check for errors and inform the user as appropriate. Finally, re-enable the save button and we are ready for the next operation. ViewController.m (section 1 of 2)
#import "ViewController.h" @implementation ViewController{ @private NSString *fileName; } @synthesize textView; - (IBAction)save:(id)sender { [sender setEnabled:NO]; NSError *error = nil; [textView.text writeToFile:fileName atomically:YES encoding:NSUnicodeStringEncoding error:&error]; if(error) { NSLog(@"%@\n%@", error, fileName); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error Saving" message:[error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; } [sender setEnabled:YES]; }

We shall examine the viewDidLoad next, and you will find this method as straightforward as the previous one. The first thing this method does is to use the path utilities to create the fully qualified path to our file. We named our file OurDocument and placed it in the Documents directory of the app. Next, we check to see whether or not the file exists. If the file does exist we read it and place its contents in the text view. We used the class method stringWithContentsOfFile:encoding:error:, but we could have just as easily used the initWithContentsOfFile:encoding:error:. In fact, the init method gives us more direct control over our memory and therefore is usually a better option. ViewController.m (section 2 of 2)
#pragma mark - UIViewController - (void)viewDidLoad { [super viewDidLoad]; fileName = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"OurDocument"]; NSFileManager *fileManager = [[NSFileManager alloc] init]; if([fileManager fileExistsAtPath:fileName]) { NSError *error = nil;

Nicolaas tenBroek (AMDG)

274

iOS Development ViewController.m (section 2 of 2)


textView.text = [NSString stringWithContentsOfFile:fileName encoding:NSUnicodeStringEncoding error:&error]; if(error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error Reading File" message:[error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; } } @end }

I/O

Thats all we need. Run this app and you will be able to edit a file, saving changes in between executions of your app. Wouldnt that have been a handy feature in our shopping list example from the chapter on Tables?

Nicolaas tenBroek (AMDG)

275

iOS Development

I/O

Intermediate File I/O (Asynchronous File Processing) Now that we have a sense of the file I/O process, we can investigate a more complex procedure. We will carry out exactly the same tasks as we did in our simple I/O example, but this time we will break up the I/O operations so that they process manageable chunks of data and demonstrate threaded I/O at the same time. Make a new single view project (we named ours IntermediateFile), and add a text view and save button just as you did with the last example. Again, all of our code will be in the view controller, but it will be slightly more complex than last time. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <NSStreamDelegate> @property (nonatomic, weak) IBOutlet UITextView *textView; @property (nonatomic, weak) IBOutlet UIButton *saveButton; - (IBAction)save; @end

In this example we are going to need access to our save button from multiple places, so we need an IBOutlet to it. Having the outlet also means we do not need the sender argument to the IBAction method. Our threaded stream handling requires that we implement the NSStreamDelegate protocol, so we have done that as well. You may also want to add an activity indicator, but we will leave that decision up to you. Our test files will be quite small, so we do not really need one in this example, but in real apps it would be a good idea to have a mechanism in place to inform users when I/O is happening. We actually need several more methods than we specified in the header file, but as they are only to support the I/O they are best located in a private category. Remember that keeping methods in a private category will make it harder for other classes to call them. This is about the best security Objective-C offers to classes, so while it is not substantial it is well worth using. Remember that in this example we will be reading and writing our data in blocks. One way to handle blocks is to decide how large the blocks should be at design time. There are more complicated run-time approaches we could take, but those are really designed for handling the large files that desktop applications tend to process and most are therefore inappropriate in a mobile environment. A design-time block size decision is often best implemented using a symbolic constant (created with #define). We named ours BUFFER_SIZE and assigned it a value of 1024 (1 Kilobyte), which is a decent starting point. Using a symbolic constant in this way prevents coding errors, and also makes tuning easier. If testing shows that you need a larger or smaller block size, you need only to change the #define and recompile. In addition to the private methods we have added a few additional instance variables as well. We have NSStream references for the input and output operations, an NSMutableData reference we will use to store the data while we are reading and writing, and an NSRange structure to assist with the writing.

Nicolaas tenBroek (AMDG)

276

iOS Development ViewController.m (Section 1 of 8)


#import "ViewController.h" #define BUFFER_SIZE 1024 @interface ViewController () (void)readFromFile; (void)endInput; (void)writeToFile; (void)endOutput; (void)streamError:(NSStream *)stream;

I/O

@end @implementation ViewController { @private NSString *fileName; NSInputStream *inputStream; NSOutputStream *outputStream; NSMutableData *streamData; NSRange dataRange; } @synthesize textView; @synthesize saveButton;

Our first code section is dedicated to handling stream operations. The threaded process of stream handling is implemented with a Structured Design sequence rather than an Object Oriented Design. All stream events are passed to a single method called stream:handleEvent: as declared in the NSStreamDelegate protocol. The event argument is an integer, so as poor as the design is, at least we can use a switch statement to decide how to process the event. We did not cover all the events here, choosing instead to focus on the ones we absolutely needed. The stream events are documented in NSStream if you would like to see the other available options. Here we decided to process bytes becoming available (meaning data is ready to be read), space becoming available (meaning the file is ready to be written to), stream errors, and end of stream being reached (which should only occur while reading, and tells us there is no more to be read). In our private category we defined methods to handle each of these events, so when each event happens we simply call the appropriate method. ViewController.m (Section 2 of 7)
#pragma mark - Stream Handling - (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent { switch(streamEvent) { case NSStreamEventHasBytesAvailable: [self readFromFile]; break; case NSStreamEventHasSpaceAvailable: [self writeToFile];

Nicolaas tenBroek (AMDG)

277

iOS Development ViewController.m (Section 2 of 7)


break; case NSStreamEventErrorOccurred: [self streamError:theStream]; break; case NSStreamEventEndEncountered: [self endInput]; break;

I/O

} }

Next we have the pair of methods that are involved with reading. The method readFomFile is called once for each block of data that is read. It is very short, but has quite a bit happening in those few lines. In order for the app to receive the data from the stream we must first create a C-style array of unsigned bytes that is large enough to hold the block of data. Using unsigned bytes guarantees that the system will not attempt to interpret the data. At this point in the process we are simply reading some raw bytes, we have no idea what they mean, so we certainly do not want them interpreted. Raw bytes are represented by the type uint8_t, which is a type definition for an unsigned 8-bit variable (or, in other words, a plain-old ordinary byte). We use our symbolic constant as the array size here, so that is the largest amount of data we will process at one time. With the array declared we ask the stream to populate the array with some data. Notice that we again use BUFFER_SIZE as an argument. C-style arrays do not contain information about their length, so we must tell the method how long the array is. Notice also that the argument is named maxLength. That tells us that the method will read up to that many bytes into the array, but it may well read less (e.g. if the end of the stream is encountered). The actual number of bytes read will be returned, and we pick up that return value in the variable bytesRead. When the read call completes we check to see if any bytes were actually read. If we received data, then we can append it to our streamData object. When reading the file all we know is that a certain number of bytes have become available. It would be dangerous to convert those bytes directly to characters, because different character encodings use differing number of bytes per character. So, we must store the bytes for later conversion to characters. The class NSData is designed to help us handle and process arrays of raw bytes, which is why you will often find it being used in I/O. Each input operation we will be reading some unknown number of bytes, so when the process begins we create a new mutable NSData instance and use it to collect the file data until we are ready to process it. The NSData object needs to be created by the method that starts the read process. In this example that method is viewDidAppear:. When the entire file has been read, the stream:handleEvent: method should receive an end event and will call our endInput method. The very first thing we do in that method is call removeFromRunLoop:forMode:. The NSStream class actually defines two methods for working with run loops. The method scheduleInRunLoop:forMode: is used to start threaded I/O processing, (though

Nicolaas tenBroek (AMDG)

278

iOS Development

I/O

nothing will happen until the stream is actually opened). The method removeFromRunLoop:forMode: is used to stop the threaded I/O processing. All threaded stream processing is handled through what are known as run loops. A run loop is not a thread, but each thread that an app creates has at least one run loop, which it uses to communicate and manage input sources. Every app has at least two threads, the one that was used to run the main method, and the one that handles graphics and input events. The run loop associated with the thread running the main method is called the main run loop. The other threads and run loops are not named. Run loops can be accessed through the class NSRunLoop which provides methods for retrieving the main run loop and the thread which called the current method (which might be the graphics handling thread for instance). Run loops associate input sources with modes. A mode is actually implemented as a collection of input sources and the thread processes only the items in one of the modes at each pass. The default mode is the one most commonly used, though custom modes may be defined if you have need of them. We use the main run loop for I/O processing because we do not want the graphics system affected if the I/O takes a long time to run. Now that the stream has been removed from the run loop, we can safely close it and then release it. We have all the bytes from the file at this point, so it is safe to convert them to characters. The NSString class provides an init method specifically designed to do that, but notice that we must tell the string which encoding scheme to use. As we discussed earlier, we are using UNICODE for our encoding scheme. Once the string has been created we can display it in the text view, and then clean up the memory used for the string and the NSData object. With everything ready for the user we then enable editing of the text view and enable the save button as well. ViewController.m (Section 3 of 7)
- (void)readFromFile { uint8_t buffer[BUFFER_SIZE]; NSInteger bytesRead = [inputStream read:buffer maxLength:BUFFER_SIZE]; if(bytesRead > 0) { [streamData appendBytes:buffer length:bytesRead]; } } - (void)endInput { [inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [inputStream close]; inputStream = nil; NSString *fileData = [[NSString alloc] initWithData:streamData encoding:NSUnicodeStringEncoding]; textView.text = fileData; fileData = nil;

Nicolaas tenBroek (AMDG)

279

iOS Development ViewController.m (Section 3 of 7)


streamData = nil; saveButton.enabled = YES; textView.editable = YES;

I/O

Writing data to a file is also handled through two methods: writeToFile and endOutput. In contrast to the last I/O example, controlled output is not simply the opposite logic that was used for input. Our output will be done in blocks just as it was when reading, but unlike what happened in the input logic, we are responsible for keeping track of how much data has been written and how much data remains. We will save our progress through the writing process using an NSRange variable. NSRange is a Cstructure containing two integer variables: location and length. While we really only need to keep track of the location in order to know how much we have written out, the NSData instance will require an NSRage variable for its processing, so we can use it for both purposes. Just as we did in readFromFile, we again need to create a buffer of raw bytes in writeToFile and will again use our symbolic constant for the buffer size. When we get to the last chunk of data, it is quite likely that we will have less than the BUFFER_SIZE bytes remaining, so we must always calculate the actual number of bytes we will be writing. It may turn out that there are no bytes remaining to be written, in which case we can close the file. Notice that this means we actually finished writing the data on the last pass through the code, but did not know until this pass. We could have redone the calculations after the write step, and discovered then that we were finished. Carrying out those calculations a second time would enable us to close the file faster, saving an iteration of the run loop, but it would have also added more code, and more steps to be executed in each iteration of the run loop (which means longer processing time). We opted for the less-code and one extra iteration approach, as it will tend to execute much faster. If there are bytes remaining we need to copy them from the NSData object. In order to get the data we must create a buffer and pass both the buffer and the NSRange variable to the NSData instance. If the range specifies a location or length that is outside the bounds of the actual data, the call to getBytes:range: will crash, which means we must be careful with our calculations here. Once we have the data we can ask the stream to write it out. Just as with the input stream, the output stream may do less work than we asked, so we must pick up the returned value that tells us the number of bytes written out. We can then use that information to update our location. Ending output is pretty much the same process as ending input. Remove the stream from the run loop, close the stream, clean up the memory, and then re-enable the UI controls as appropriate. Closing the output file actually carries a bit more significance than closing an input file though. Closing an input file simply releases the resources in the OS, letting the system know we are done with the file. When you close an output file a special character called the End Of File (EOF) character is written to the file. This character is how input streams know they have read all the data from the file. Not having an EOF at the

Nicolaas tenBroek (AMDG)

280

iOS Development

I/O

end of a file will cause a huge error when trying to read the file the next time, so this close operation is extremely critical. ViewController.m (Section 4 of 7)
- (void)writeToFile { NSInteger bytesRemaining = [streamData length] - dataRange.location; if(bytesRemaining > 0) { dataRange.length = (BUFFER_SIZE < bytesRemaining)? BUFFER_SIZE : bytesRemaining; uint8_t buffer[dataRange.length]; [streamData getBytes:buffer range:dataRange]; NSInteger bytesWritten = [outputStream write:buffer maxLength:dataRange.length]; dataRange.location += bytesWritten; } else { [self endOutput]; }

- (void)endOutput { [outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [outputStream close]; outputStream = nil; streamData = nil; } saveButton.enabled = YES;

The last method in the Stream Handling section is one we hope not to see in operation. It handles the stream errors. Well, we say handles, but in reality all we did was to alert the user of the problem and then give up. Before we alert the user we must figure out what it was we were attempting to do. This method could be called at any point in the process, so we check to see if we are reading or writing, and then clean up the memory appropriately. Notice that we also remove the stream from the run loop. If things have gone awry, there is no sense in trying the failing operation over and over again. ViewController.m (Section 5 of 7)
- (void)streamError:(NSStream *)stream { if(inputStream) { [inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [inputStream close]; inputStream = nil; } if(outputStream) { [outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [outputStream close]; outputStream = nil;

Nicolaas tenBroek (AMDG)

281

iOS Development ViewController.m (Section 5 of 7)


NSFileManager *fileManager = [[NSFileManager alloc] init]; NSError *error = nil; [fileManager removeItemAtPath:fileName error:&error];

I/O

} streamData = nil;

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"I/O Error!" message:[[stream streamError] localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }

With the stream handling methods completed we can turn our attention to our sole IBAction method. This method is called when the user presses the Save button. Just as in the last example we immediately disable the save button to prevent multiple presses. This time however we make a copy of the data from the text view. We will handle all the output processing from the copy, which means we do not need to disable the text view. That way we can allow the user to continue typing while we save the copy we made. The NSString class contains the method dataUsingEncoding: which is the logical opposite of the init method we used earlier. It converts from the string data to raw bytes using the specified character encoding. We retain the NSData object because we will need to use that object as the data source for the write operations. With the data ready to go we create the output stream, set self as the delegate, and schedule the stream in the run loop. Notice that we do not open the stream until after it has been scheduled. This ordering is important. Opening the stream will inform the run loop that the stream is ready to be processed, so the stream must already be in a run loop. ViewController.m (Section 6 of 7)
#pragma mark - IBActions - (IBAction)save { saveButton.enabled = NO; streamData = (NSMutableData *)[textView.text dataUsingEncoding:NSUnicodeStringEncoding]; dataRange.location = 0; outputStream = [[NSOutputStream alloc] initToFileAtPath:fileName append:NO]; outputStream.delegate = self; [outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [outputStream open]; }

Nicolaas tenBroek (AMDG)

282

iOS Development

I/O

Just as in our last example, we will create and store the file, though this time we accomplished that step in initWithNibName:bundle:. We are using the same file name as we did last time, and are again storing the file in the Documents directory. The viewDidAppear: method is a bit more complicated than it was in our last example however. This time we must create our empty and mutable NSData object, create an input stream, schedule the input stream in a run loop, and then open it. Just as with the save method, opening the stream must come after scheduling the stream in the run loop. You may also notice that we also disabled both the save button and the text view. We dont want the user trying to type things in or attempting a save operation while we are reading. If they manage to enter any data before the file is read, it will simply disappear when we assign the file data to the text view. That kind of behaviour will certainly not make for a happy user. ViewController.m (Section 7 of 7)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if((self = [super initWithNibName:nibName bundle:bundle])) { fileName = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"OurDocument"]; } return self; } - (void)viewDidAppear:(BOOL)animated { NSFileManager *fileManager = [[NSFileManager alloc] init]; if([fileManager fileExistsAtPath:fileName]) { saveButton.enabled = NO; textView.editable = NO; streamData = [[NSMutableData alloc] init]; inputStream = [[NSInputStream alloc] initWithFileAtPath:fileName]; inputStream.delegate = self; [inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [inputStream open]; } } @end [super viewDidAppear:animated];

So, with those seven methods we have implemented threaded file I/O. While it is much more complicated than using the built-in methods we used before, it is also much more powerful. For larger data files this approach is absolutely required to preserve the performance of your application. Of

Nicolaas tenBroek (AMDG)

283

iOS Development course, taken individually, each of those methods is performing a small number of well-defined tasks, which certainly makes the process easier to understand.

I/O

At this point you should have a good idea of the kinds of I/O processing available to you through iOS. We have discussed the file system made available to every app, the classes which provide native support for I/O, and looked at two completely different approaches for reading and writing. I/O operations are incredibly important to mobile apps both because of the restrictions imposed by mobile devices, and because of the way people use mobile apps. Virtually every app you write will need to use it, in fact, most of the examples we have developed in the earlier chapters of this book would be much better if we saved the data in the app across executions. I/O is the only mechanism available to make that happen.

Nicolaas tenBroek (AMDG)

284

iOS Development

I/O

Nicolaas tenBroek (AMDG)

285

iOS Development

Preferences and Settings

Preferences and Settings


Virtually every app supports the ability to store user preferences, settings, and even app state for use the next time the app is launched. The practice has become so ubiquitous that most users do not even realise that such behaviours are features; they simply expect that apps will function that way. It is safe to say that very few apps can afford to leave such features out without annoying users to such an extent that they will simply abandon the lacking app for another one. For now, we can ignore the practice of saving app state and focus instead on preferences and settings. We will return to app state later. There are basically two mechanisms for presenting settings options to the user, and each mechanism has its own particular coding approach. One approach is to present one or more views within the app containing controls for the user settings. The second approach is to use the built-in Settings app, and no views within the app. Each approach has its own advantages and disadvantages. The in-app presentation of settings controls is best used when settings are expected to change often. Games are a great example of this. Users are often toggling game sounds on and off or adjusting the volume in response to changes in their environment, or simply to suit their desires (that background song might start to get on peoples nerves after all). In-app presentation facilitates rapid access to controls, but comes at a cost of screen space. You must plan a control into your screen design that enables a user to access the settings screen. Generally that control needs to be always available, so this can be a serious effort depending on your design. You must also design your settings screen, which presents a double-edged sword. The advantage of designing your own screen is that you can create controls that fit with the overall look and feel of your app. The disadvantage of course is the cost of development time and effort, which can be substantial. The built-in settings app is best used for settings that do not need to change often. These are the kinds of choices users will make once or twice during the life of the app, like setting up cache size, choosing a search engine, or selecting whether to receive push updates. This approach has several advantages. First, there is no cost in screen space, which gives you a great deal of flexibility when designing your app. Second, there is virtually no cost in design. The built-in settings app takes care of the presentation and operation of the controls as well as reading and saving the data. Finally, the use of standardized controls and locations means users will already be familiar with the process of configuring your app. This will help your app look and feel as if it were designed to be part of iOS. The disadvantage to this approach is that the user may not be immediately aware that the settings are even available. If you do not notify the user in some fashion (i.e. a note on the splash screen or other such mechanism), they may not know the settings are available until they happen to stumble upon them while using the settings app for something else, or find out through some other means. This notification problem is often overlooked by app designers and can lead to needlessly frustrated users.

Nicolaas tenBroek (AMDG)

286

iOS Development

Preferences and Settings

Property Lists
Regardless of the approach you take in storing user preferences, you will most likely need to make use of property lists and property list objects. A property list in iOS is really just a standardized file format that is designed to allow easy storage and retrieval of the kinds of data that are commonly used as settings. Property list files use the extension .plist, so they are often referred to as simply plists. There are three property list file formats which have been imported from Mac OSX: XML, binary, and an old-style ASCII format. The old style is truly old and failing out of favour, so we will ignore it. While the binary and XML formats are both supported, the XML format is the one most often used with iOS apps. Of course, each format has its strengths and weaknesses. The binary file format is faster for I/O operations and produces much smaller files than XML. However, standard editors cannot edit the binary files, which will force you to create them within your app at runtime. The XML format can be edited using XCode, or any other text editor for that matter. That means you can build the file with default values at design time, but the size of text files increases quickly and can take much longer to process than binary files. Those issues being noted, the fact is that most preferences and settings files in iOS apps are actually very small. So small in fact, that the size alone erases the I/O performance differences between the two formats, which is why XML is most often used. The data in a property list are stored as objects that are instances of the property list classes. Once read into memory, property lists are themselves an instance of one of the property list classes. The classes used to represent property list data are: NSData, NSDate, NSNumber, NSString, NSArray, and NSDictionary. The NSArray and NSDictionary instances must contain property list objects to be part of a property list. For example, an array that contains instances of NSDate, NSNumber, NSString, NSArray, or NSDictionary can be a part of a property list (yes, its a bit recursive). An array containing instances of a class you wrote may not be part of a property list. The NSDictionary has an added restriction that its keys must be NSString instances if it is to be part of a property list. The inclusion of NSData in the list of property list objects means that you can add your own objects to property list objects if you implement the NSCoding protocol. Then you simply need to convert your object to an instance of NSData, and add it to the collection.

Nicolaas tenBroek (AMDG)

287

iOS Development

Preferences and Settings

UIApplication and UIApplicationDelegate


The UIApplication and its delegate are parts of iOS apps that we have been mostly ignoring up until this point, because we have not really needed more from them than the templates provided. Once you start including preferences within your application though, the app delegate becomes very useful, which makes this a good time to discuss its operation. Every C-based application starts in a function called main, and ours are no different. Objective-C apps immediately pass control to UIApplicationMain, which then creates a single instance of the class UIApplication and its delegate. That single instance of UIApplication becomes the locus of control for the app, coordinating windows, app-wide functionality, and notification of events. For instance, the UIApplication can hide the status bar for the entire app, set badge numbers on the apps icon, or notify the app of device rotation. You will rarely, if ever, need to subclass UIApplication. The class is designed to notify its delegate of all the interesting events, so most every time you will simply implement the delegate protocol and through that gain access to everything you need. The instance of UIApplication can be retrieved through the class method sharedApplication, which then also gives us access to the delegate property. In the UIApplicationDelegate we receive notifications about the app changing state like launching, entering the background, or terminating (though that method is no longer called for devices running iOS versions 4.0 or later). That makes the delegate the perfect place to handle preferences. There is only one instance of the class so we do not need to worry about data clashing, the instance is easily accessed from any location within our app, and we can load and save preferences in response to changes in the application state. Generally we will load the preferences in the methods application:didFinishLaunchingWithOptions: and applicationDidBecomeActive:, though we only use the applicationDidBecomeActive: if we are using the built-in Settings app as the settings may be changed while the application is suspended. We can save the preferences either each time the values change or when the application is suspended by implementing either applicationWillResignActive: or application:didEnterBackground:.

Nicolaas tenBroek (AMDG)

288

iOS Development

Preferences and Settings

In-App Preferences
Handling preferences within the app is not all that difficult. In fact, you will likely spend far more time designing the UI than working with the storage and retrieval of the data itself. While the options for handling in-app preferences are literally limitless, we will demonstrate a common approach that is able to serve as a template for most app needs. For this example, create a single view app called SimplePreferences. We will start our template in the app delegate, by adding method for storing a simple preference and another for retrieving the preference. We are storing our preferences in a dictionary, so both methods will require a string for the key. Remember that in order for dictionaries to be used as property list objects, they must use strings for the keys. AppDelegate.h
#import <UIKit/UIKit.h> @class ViewController; @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) ViewController *viewController; - (void)setPreference:(id)preference forKey:(NSString *)key; - (id)preferenceForKey:(NSString *)key; @end

The method setPreference:forKey: carries out two separate tasks. First, it stores the preference in the dictionary for later use, and second, saves the property list to a file. By saving each time a preference is set, we never have to worry about whether or not the file needs to be saved. Of course, this approach does have its downsides as well. If an array is stored as a preference, we have no mechanism to handle saving if the contents of the array are changed without the array begin re-set. We will demonstrate a fix for that problem in the next example. The getter method preferenceForKey: is as straightforward as a getter could possibly be. Note that we used id as the return type. This means the returned value can be any object reference. AppDelegate.m (section 1 of 2)
#import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate { @private NSMutableDictionary *preferences; NSString *preferencesFile; } @synthesize window = _window; @synthesize viewController = _viewController;

Nicolaas tenBroek (AMDG)

289

iOS Development AppDelegate.m (section 1 of 2)


#pragma mark - Preferences - (void)setPreference:(id)preference forKey:(NSString *)key { [preferences setObject:preference forKey:key]; [preferences writeToFile:preferencesFile atomically:YES]; } - (id)preferenceForKey:(NSString *)key { return [preferences valueForKey:key]; }

Preferences and Settings

The method application:didFinishLaunchingWithOptions: does a fair amount more work than the previous two. This method comes in as part of the app template, and includes the last three lines of code that take care of displaying the initial view and window. We have left those lines untouched, and added in only the code to load the property list. First we create and retain a path to our plist. The actual location of Property List files will change depending on how they are created. As we are creating ours from scratch we can pick the location for the file. The proper location for preferences files is <app_home>/Library/Preferences. We know that this folder will be backed up and maintained across installs, which makes it a good safe location for this information. However, you must remember that every app will already have some files stored here, and your file names must not conflict with those. Currently you will find two files in this directory: .GlobalPreferences.plist and com.apple.PeoplePicker.plist, but future versions of iOS may well add more to the default setup. You should use a naming scheme that will be unique. For instance, you could preference your file name with your domain name, and use a random number after the name. With the file path created, we attempt to load the property list as a mutable dictionary. If the file does not exist or the load fails, we will have a nil pointer. If we have a nil pointer after attempting the load, we simply create a new dictionary; otherwise we retain the dictionary for later use. Of course, creating a new dictionary like this means that all settings are lost, and we have not replaced them with anything. For all intents and purposes, such an app would be less than factory new, because it would not even contain default values. We will fix that particular problem in a later example. AppDelegate.m (section 2 of 2)
#pragma mark - App Delegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { preferencesFile = [[[NSHomeDirectory() stringByAppendingPathComponent:@"Library"] stringByAppendingPathComponent:@"Preferences"] stringByAppendingPathComponent:@"prefs.plist"]; preferences = [NSMutableDictionary dictionaryWithContentsOfFile:preferencesFile]; if(!preferences) { preferences = [[NSMutableDictionary alloc] init]; }

Nicolaas tenBroek (AMDG)

290

iOS Development AppDelegate.m (section 2 of 2)

Preferences and Settings

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES; } @end

That little bit of code sets up a pretty fair template. The preferences can be easily accessed and modified from any location within the app, and we have taken care of the file handling as well. You will doubtless recall from the I/O chapter that this approach will only work well for small files and infrequent property changes. If the property list file becomes large, or is accessed frequently, then you will have to use more advanced asynchronous file handling methodologies. With the template complete, we can turn our attention to the view controller, and demonstrate accessing and storing the preferences. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) IBOutlet UITextField *textField; @property (nonatomic, weak) IBOutlet UISlider *slider; - (IBAction)sliderValueChanged; @end

For our demonstration app we have added a text field and a slider, simply as a way to show processing two different kinds of data. This same process will work for any property list data type. We created IBOutlet properties, a delegate for the text field, and an IBAction for the slider (sliders do not use delegates; instead they behave more like buttons). In both our textFieldShouldReturn: and sliderValueChanged we save the newly entered property values. The process is the same for both, though the data type is different. In each of those methods we access the shared instance of the UIApplication, and then access its delegate property. The delegate property type is UIApplicationDelegate, so to get access to our new methods we must either store the delegate in a local variable of our specific app type, or cast it. As we only need access to the delegate for a single line of code, we opt for the casting approach here. Our example is extremely short, so we opted for hard-coding our keys. In a real-life scenario you must never do this. There are a variety of better approaches like saving the keys in class-level constants to prevent problems should you ever change the keys value. This issue should not be taken lightly. Those kinds of changes always happen well-after a great deal of code has been written, and at that point the

Nicolaas tenBroek (AMDG)

291

iOS Development

Preferences and Settings

hard coded key will somehow have managed to appear all through your code. We will demonstrate proper solutions to this problem in the next examples. ViewController.m (section 1 of 2)
#import "ViewController.h" #import "AppDelegate.h" @implementation ViewController @synthesize textField; @synthesize slider; #pragma mark - TextField Delegate - (BOOL)textFieldShouldReturn:(UITextField *)theTextField { [textField resignFirstResponder]; [(AppDelegate *)[UIApplication sharedApplication].delegate setPreference:textField.text forKey:@"USER"]; } return YES;

#pragma mark - IBActions - (IBAction)sliderValueChanged { [(AppDelegate *)[UIApplication sharedApplication].delegate setPreference:[NSNumber numberWithFloat:slider.value] forKey:@"VALUE"]; }

In viewDidLoad we use the getter methods. This time we needed access to the delegate more than once, so it made more sense to store it in a local variable. This setup allows us to avoid the casting that we needed in the other methods. Other than that change, the process is essentially the same as we used in the other methods. ViewController.m (section 2 of 2)
#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; AppDelegate *appDelegate = [UIApplication sharedApplication].delegate; textField.text = [appDelegate preferenceForKey:@"USER"]; [slider setValue:[[appDelegate preferenceForKey:@"VALUE"] floatValue] animated:NO]; } @end

Nicolaas tenBroek (AMDG)

292

iOS Development

Preferences and Settings

With those few changes in place we now have an app that saves and restores user settings. When testing it, enter some data and then kill the app. You may want to double-press the Home button to ensure the app is completely off. Then simply restart it, and you will see the settings restored. For our next example we will have a very similar template, but this time we will create a default property list file within XCode, and also demonstrate some more advanced approaches to dealing with changes in the app state. We will modify our approach to saving and loading in a way that will accommodate changes happening outside of the setter method, and also handle our keys in a more appropriate way. For this example we will again create a new view-based project, and name it what you will. We are super-creative, so we named ours SimplePreferencesII. With the project created we can add our default plist file. To add the file, right-click on the project, choose New File from the menu, and then choose a Property List from the Resource templates.

Keeping up our unimaginable streak of naming creativity we named our new file default_preferences.plist (the name is important because we will use it in code soon). If you select the new file for editing, you will be presented with a nearly empty screen. There are columns across the top labelled Key, Type, and Value, but no other controls.

Here is where things get a bit funny. Normally, when editing a property list, if you wanted to add a new property, you would hover the mouse over the Key column and press the plus (+) icon that appears there. Similarly, to remove a property you would press the minus (-) icon. However, in the empty

Nicolaas tenBroek (AMDG)

293

iOS Development

Preferences and Settings

property list file, there are no controls, and no apparent way to add that first property. Thankfully, while the fix to this problem is not documented, it also is not complicated. Simply press the return key and a new property will be added. You can also use the return key to add a new property at any time as long as none of the columns are being edited. Go ahead and press enter now, and you should see something like this:

Now we can start editing our file. Set the key to user (without the quotes) and the value to unknown (also without the quotes). The default type is String, so we do not need to change that. Add a new property, set the key to value (yes, thats terribly funny), change the type to number, and set the value to 0.3 (all without the quotes). Save the file, and we have a preferences file similar to what we created by hand in the previous example.

Nicolaas tenBroek (AMDG)

294

iOS Development

Preferences and Settings

We will now start our coding in the same place as we did previously by modifying the app delegate. AppDelegate.h
#import <UIKit/UIKit.h> @class ViewController; @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) ViewController *viewController; @property (nonatomic, readonly) NSString *userKey; @property (nonatomic, readonly) NSString *valueKey; - (void)setPreference:(id)preference forKey:(NSString *)key; - (id)preferenceForKey:(NSString *)key; @end

Most of the changes we have made are the same ones that we needed in the last example. However, you will notice that we have added two new read-only properties: userKey and valueKey. These properties will serve as the keys for accessing our preferences and replace the hard-coded ones we used in the previous example. By housing the keys in the app delegate and accessing them through properties we have centralised the storage of the actual values of the keys. If we decide to change the values of those keys in the future, we need only modify the app delegate and recompile, which will save us a tremendous amount of effort. There are quite a few changes from what we had in the previous example, but the work itself is essentially the same. We mostly moved things around to avoid having code in multiple places. First we created a private category for a save preferences method and a load preferences method. The save code isnt really complicated enough (yet) to warrant its own method, but there is some nice symmetry to the code considering we did need a separate method for the load. Also, if we change our mind about how we will perform the save operation, having the method in place will greatly simplify that update. The loadPreferences is mostly the same code we used in application:didFinishLaunchingWithOptions: in the previous example, but there are some important changes. First, we are dealing with two files now. One is the users preference file (which we helpfully called user_prefs.plist), and the second is the default file we created through XCode. We also need to be cognizant of the fact that in this new setup the load method might be called multiple times. That change means that we cannot simply create and retain the path to the file each time the method is called (as we had in our earlier example) or we will be creating a memory leak. Instead we need to first check and see if we already have the path saved, and skip creating it if the reference exists. Next, we check to see if the users file already exists, and only load the file if it does. If the file does not exist we load the preferences from the default file. The process for finding the default file is a bit different from what we have used before. When we created the plist file in XCode we did not really assign it a location within the apps file system.

Nicolaas tenBroek (AMDG)

295

iOS Development

Preferences and Settings

Remember though that all resources become part of the application bundle, which is actually stored in the <Application Name>.app directory when the app is built. Thankfully we do not need to hard code the path in order to access the file. Instead we can use a method in the NSBundle class that will search for a file and return the path as a string. The method pathForResource:ofType: can be used to locate anything within the bundle. Simply specify the file name as the resource argument, and the file extension (without the period) as the type argument, and the method will do the rest. This method will come in very handy if you want to start loading other resources (e.g. images and sounds) dynamically in more complex apps. We should always have a file after the if runs, but file I/O is a place where any app on any platform can fail, so we always verify. After all, we may have typed in the file name or some other item incorrectly, which would prevent the file from loading. If the preferences dictionary does not exist, then we know we have a problem, so we can display an error. This error should only appear during app testing, so the message is more for us than a user. AppDelegate.m (section 1 of 3)
#import "AppDelegate.h" #import "ViewController.h" @interface AppDelegate () - (void)savePreferences; - (void)loadPreferences; @end @implementation AppDelegate { @private NSMutableDictionary *preferences; NSString *preferencesFile; } @synthesize window = _window; @synthesize viewController = _viewController; #pragma mark - Private Interface - (void)savePreferences { [preferences writeToFile:preferencesFile atomically:YES]; } - (void)loadPreferences { if(!preferencesFile) { preferencesFile = [[[NSHomeDirectory() stringByAppendingPathComponent:@"Library"] stringByAppendingPathComponent:@"Preferences"] stringByAppendingPathComponent:@"user_prefs.plist"]; } NSFileManager *fileManager = [[NSFileManager alloc] init]; if([fileManager fileExistsAtPath:preferencesFile]) { preferences = [NSMutableDictionary dictionaryWithContentsOfFile:preferencesFile]; } else {

Nicolaas tenBroek (AMDG)

296

iOS Development AppDelegate.m (section 1 of 3)


}

Preferences and Settings

preferences = [NSMutableDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"default_preferences" ofType:@"plist"]];

if(!preferences) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error!" message:@"Preferences File NOT Loaded!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; preferences = [[NSMutableDictionary alloc] init]; } }

The methods setPreference:forKey: and preferenceForKey: are virtually identical to what we used in the previous example. The one small change is that we call our new save method in the setter rather than saving the file directly. Next we have our key properties. Notice that we could not synthesize these, as the data they return is not stored anywhere. These methods are obviously incredibly simple, but as we mentioned earlier they can save us a great deal of effort in later modifications. Do not be tempted to do away with them simply because they are small. These small efforts will save us lots of headaches in real-world apps. AppDelegate.m (section 2 of 3)
#pragma mark - Preferences - (void)setPreference:(id)preference forKey:(NSString *)key { [preferences setObject:preference forKey:key]; [self savePreferences]; } - (id)preferenceForKey:(NSString *)key { return [preferences valueForKey:key]; } - (NSString *)userKey { return @"user"; } - (NSString *)valueKey { return @"value"; }

In application:didFinishLaunchingWithOptions: we again see code nearly identical to what we wrote in the last example. The only difference is that we pulled all the loading code into its own method, which means we can simply call the method from here. The last three lines are supplied by the template and have not been modified.

Nicolaas tenBroek (AMDG)

297

iOS Development

Preferences and Settings

At this point we will add two new methods to the mix. The method applicationDidEnterBackground: will be called whenever the user presses the home button or the device switches apps (i.e. if the user receives a phone call or SMS message). In this method we save our preferences. This save will handle any modification to the preferences data that was not recorded by setPreference:forKey:. The method applicationDidBecomeActive: is called whenever an application that was in the background is restarted. Here we again load our preferences. While our simple demo application does not support a means for having the preferences file modified while the application is suspended, the mechanism used here demonstrates how you would handle such a situation. The application template provided by XCode provides the method applicationWillTerminate: with the helpful instructions that you might want to save data in that method. Remember that this particular method is only called in apps running in iOS version 3.x or earlier. If you plan to support one of these earlier versions then the code in this method should be the same saving code as we have in applicationDidEnterBackground:. If you do not plan to support the earlier versions of the OS, then simply remove this method. AppDelegate.m (section 3 of 3)
#pragma mark - App Delegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self loadPreferences]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES;

} @end

Now that the app delegate is complete we can move on to our view controller. The code here is virtually identical to the code we used in the previous example. In fact, because our code was so well designed (and so very simple) previously the only change we need to make is in handling the keys for our preferences. In the previous example we hard-coded the keys, now we can access the app delegates properties instead. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) IBOutlet UITextField *textField; @property (nonatomic, weak) IBOutlet UISlider *slider;

Nicolaas tenBroek (AMDG)

298

iOS Development ViewController.h


- (IBAction)sliderValueChanged; @end

Preferences and Settings

ViewController.m
#import "ViewController.h" #import "AppDelegate.h" @implementation ViewController @synthesize textField; @synthesize slider; #pragma mark - TextField Delegate - (BOOL)textFieldShouldReturn:(UITextField *)theTextField { [textField resignFirstResponder]; AppDelegate *appDelegate = [UIApplication sharedApplication].delegate; [appDelegate setPreference:textField.text forKey:appDelegate.userKey]; return YES; } #pragma mark - IBActions - (IBAction)sliderValueChanged { AppDelegate *appDelegate = [UIApplication sharedApplication].delegate; [appDelegate setPreference:[NSNumber numberWithFloat:slider.value] forKey:appDelegate.valueKey]; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; AppDelegate *appDelegate = [UIApplication sharedApplication].delegate; textField.text = [appDelegate preferenceForKey:appDelegate.userKey]; [slider setValue:[[appDelegate preferenceForKey:appDelegate.valueKey] floatValue] animated:NO];

} @end

Notice that we are no longer casting our app delegate access, as we need to use it more than once each time. This change is so minor you might miss it, but as we mentioned previously, it can have significant time savings in the future. With those changes we have a template that functionally appears the same as the previous example, but adds the ability for us to easily create a default plist within XCode, and Nicolaas tenBroek (AMDG) 299

iOS Development

Preferences and Settings

handles changes to the preferences much better than we had before. While we did not handle it in this example, it would be trivial to add a control to allow the user to reset the settings to the default, which is always a nice addition. Due to its simplicity, this template can be easily ported to handle preferences in most apps, which should make it quite useful.

Nicolaas tenBroek (AMDG)

300

iOS Development

Preferences and Settings

Settings Bundle
The alternative to setting preferences within the app is to use what is called a Settings Bundle. The Settings Bundle gives us access to the built-in Settings app, and removes the need to provide space within the UI for handling settings. Much of the programming work is similar, so this change is really more about user interface design than it is about programming effort. A settings bundle is really a directory named Settings.bundle that contains a prescribed set of files and subdirectories. The directory must contain a file named Root.plist, but can also contain additional plists as needed. The Root.plist file defines the main screen of settings, while the optional additional plist files define child-setting screens. The bundle must also include at least one subdirectory named XX.lproj. The XX would be replaced with a language code that identifies the language of the words to be displayed on the settings screen. The .lproj directory is required to contain a .strings file which by default is called Root.strings. You can change the name if you wish, but the same file name must be used for all the .lproj directories. The .strings file will contain the translated strings for display; though creating the translation is up to you. If you plan to use sliders in your settings you will need to provide minimum and maximum images for the sliders. Those images must also be in the bundle. Access to the data contained in the settings bundle is handled through the class NSUserDefaults. This class provides much of the behaviour we had to produce in our app delegate in the previous examples. The NSUserDefaults class provides access to a singleton that automatically loads and saves the data from the settings bundle. Using a settings bundle is specifically providing a mechanism that allows the settings to be modified outside of the app, and NSUserDefaults takes that into account. At specific intervals the user defaults instance synchronises itself with the settings bundle. If any values changed in the bundle the new values are automatically loaded, and if the app has unsaved changes, they are saved to the bundle. Additionally, NSUserDefaults contains convenience methods for setting and retrieving the data. Those methods take care of the data-type casting, so if you need a float, you can simply ask for a float. To actually see a settings bundle we will need to create a new app, so go ahead and make one now. A simple single view app will again be sufficient for our purposes. We called ours SettingsDemo, but you may of course call yours what you wish. Once the project has been created, right-click on the project in the project explorer and choose New File just as we have done many times before. Go to the Resource section of the new file dialogue just as we did in the last two examples, but this time choose Settings Bundle. Take all the defaults presented by XCode. Rather than a single file, the bundle is a directory and will have a disclosure button on the left side of its icon. Expand the bundle and the entire directory should now be visible. Navigating through the structure you should see the Root.plist and en.lproj (assuming your computer is set to English). If you open Root.plist you will find that it is prefilled with examples. In fact, we can simply build and run your app at this point. After the app is running in the simulator, press the home button to close it and switch to the Settings app. There you will find an entry for your SettingsDemo app.

Nicolaas tenBroek (AMDG)

301

iOS Development

Preferences and Settings

Tap that entry and you will see the example settings that are in the plist.

Notice that the layout mechanism used is a grouped table. Each cell in the table contains the controls for a single setting, and the groups of controls can be labelled. In the example the group is labelled Group, which is perhaps not terribly creative, but is instructive. Next we see a label and a text field, followed by a label and a switch, and finally a slider. Notice that we have no way of knowing what the Nicolaas tenBroek (AMDG) 302

iOS Development

Preferences and Settings

slider represents. Sliders do not support labels, which is why we need to supply custom minimum and maximum images. Those images will be the only information given the user to explain the slider, so you must ensure they are easy to understand. All of the items on this screen are specified within the plist file. Of course, this means the plist is necessarily a bit more involved than the ones we created in the previous examples. Lets take a look at the format of that file now. The Settings Bundle Root.plist file contains two high-level items. The first item is called Preference Items and is an array. This array is used to hold all the settings items that will be displayed. The second item is called Strings Filename and lists the file name that will be used to hold the localized language strings. By default this entry is set to Root which matches the name of the strings file that is supplied by the template. Each element in the Preference Items array is a dictionary, and each dictionary contains the information for one of the graphical controls that will be displayed on the settings page. There are eight types of controls you can choose from, and each control has its own list of attributes that can be set in the dictionary. The attributes fall into one of two groups, they are either display-only, or allow data entry by the user.

Display Only Controls


PSChildPaneSpecifier
Attribute Name Type Title File Creates an additional page of settings. The child settings must be in a separate plist file. Required Localizable Data Type Description Must be set to: YES NO String PSChildPaneSpecifier The name of the setting to be YES YES String displayed on the settings screen The name of the plist file YES NO String containing the settings for the child screen.

PSGroupSpecifier
Attribute Name Type Title FooterText

Creates a visual grouping to indicate related settings controls. Can optionally display a title for the group. Required Localizable Data Type Description Must be set to: YES NO String PSGroupSpecifier The name to be displayed at above the NO YES String group of controls. Text to be displayed beneath the NO YES String group of controls.

Nicolaas tenBroek (AMDG)

303

iOS Development

Preferences and Settings Displays a read-only value in a cell. Note: This control is not used for labelling other controls; instead it is used to inform the user about the application. For instance, you might use it to display the version number. Required Localizable Data Type Description Must be set to: YES NO String PSTitleValueSpecifier The name of the setting to be NO YES String displayed on the settings screen. This is the key that will be used in YES NO String code to set or retrieve the value of this item. The default to use if the value has YES YES String not been set. This array contains the data values to be used in the app and NO NO Array will not be presented to the user. Therefore the array can contain any type required by the app. This array must contain strings that will be presented to the user. NO YES Array Each string will be associated with its corresponding data value by index.

PSTitleValueSpecifier
Attribute Name Type Title Identifier (Key before iOS 5) DefaultValue

Values

Titles

Data Entry Controls


PSMultiValueSpecifier
Attribute Name Type Title Identifier (Key before iOS 5) DefaultValue Displays a list of values in a table using a separate view to display the table. Allows the user to select one of the items at a time. Required Localizable Data Type Description Must be set to: YES NO String PSMultiValueSpecifier The name of the setting to be YES YES String displayed on the settings screen This is the key that will be used in YES NO String code to set or retrieve the value of this item. The value used if the setting does YES NO any not have a value. This array contains the data values to be used in the app and YES NO Array will not be presented to the user. Therefore the array can contain any type required by the app.

Values

Nicolaas tenBroek (AMDG)

304

iOS Development

Preferences and Settings Displays a list of values in a table using a separate view to display the table. Allows the user to select one of the items at a time. Required Localizable Data Type Description This array must contain strings that will be presented to the YES YES Array user. Each string will be associated with its corresponding data value by index.

PSMultiValueSpecifier
Attribute Name

Titles

PSRadioGroupSpecifier

Attribute Name Type

Title

FooterText Identifier (Key before iOS 5) DefaultValue

Values

Titles

Displays a list of values in a table, designed for small numbers of choices and displays its table in the same view as the other options. Allows the user to select one of the items at a time. This control was introduced in iOS 5 and at the time of this writing is not fully supported by the property list editor in XCode. If you wish to use it you will need to edit the XML manually until XCode includes full support. Required Localizable Data Type Description Must be set to: YES NO String PSRadioGroupSpecifier The name of the group to be displayed on the settings screen. NO YES String If no title is specified a gap will be introduced between the group and the other settings. Optional text to display below NO YES String the group of choices. This is the key that will be used in YES NO String code to set or retrieve the value of this item. The value used if the setting does YES NO any not have a value. This array contains the data values to be used in the app and YES NO Array will not be presented to the user. Therefore the array can contain any type required by the app. This array must contain strings that will be presented to the YES YES Array user. Each string will be associated with its corresponding data value by index.

Nicolaas tenBroek (AMDG)

305

iOS Development

Preferences and Settings Presents a slider control for choosing from a range of values. The slider cannot display an associated title, which means users will not know the purpose of the slider by reading a setting name. However, the slider can display images to indicate the minimum and maximum values, and the images can convey the purpose of the setting. Required Localizable Data Type Description Must be set to: YES NO String PSSliderSpecifier This is the key that will be used in code YES NO String to set or retrieve the value of this item. YES NO Real Number The default value for this setting. The value used when the slider is at its YES NO Real Number minimum setting. The value used when the slider is at its YES NO Real Number maximum setting. The name of the image file to be NO NO String displayed on the minimum side of the slider. Should be 21 pixels square. The name of the image file to be NO NO String displayed on the minimum side of the slider. Should be 21 pixels square.

PSSliderSpecifier
Attribute Name Type Identifier (Key before iOS 5) DefaultValue MinimumValue MaximumValue MinimumValueImage

MaximumValueImage

PSTextFieldSpecifier
Attribute Name Type Title Identifier (Key before iOS 5) DefaultValue IsSecure

Presents a text field. Required Localizable YES NO YES NO NO NO YES NO YES NO

Data Type String String String String Boolean

KeyboardType

NO

NO

String

AutoCapitalizationType AutoCorrectionType

NO NO

NO NO

String String

Description Must be set to: PSTextFieldSpecifier The name of the setting to be displayed on the settings screen. This is the key that will be used in code to set or retrieve the value of this item. The value to be displayed in the text field by default. Indicates whether or not this text field is a password-entry field. Controls the visibility of characters. Must be one of: Alphabet, NumbersAndPunctuation, NumberPad, URL, or EmailAddress. Defaults to: Alphabet. Must be one of: None, Sentences, Words, or AllCharacters. Defaults to: None. Must be one of: Default, No, or Yes. Defaults to: Default.

Nicolaas tenBroek (AMDG)

306

iOS Development

Preferences and Settings Presents a toggle switch typically used to set a Boolean value. Required Localizable Data Type Description Must be set to: YES NO String PSToggleSwitchSpecifier The name of the setting to YES YES String be displayed on the settings screen. This is the key that will be used in code to set or YES NO String retrieve the value of this item. The value used to set the YES YES any scalar type switch if the key is not present in the settings. The value displayed when the switch is in the on NO YES any scalar type position. Defaults to the string: ON. The value displayed when the switch is in the off NO YES any scalar type position. Defaults to the string: OFF.

PSToggleSwitchSpecifier
Attribute Name Type Title

Identifier (Key before iOS 5)

DefaultValue

TrueValue

FalseValue

Now that we have seen the items that can be part of a Settings plist you might be tempted to jump in and start editing. Before you begin though, we need to have a quick word about the process. Obviously adding and removing items from the plist can be done using the same processes that we used in our previous example. You can use the plus and minus icons to add or remove an item from a dictionary, or create entirely new entries. Pressing enter will also add a new entry, though the location of the new entry will depend on what part of the document is currently active. It is possible however to edit plists in other editors, and sometimes other editors will be preferable. Remember that plists are XML files, which means most any text editor or XML editor can be used. The PSTitleValueSpecifier and PSMultiValueSpecifier elements both contain two arrays of strings (Values and Titles) that must be kept synchronized as the index from one will be used to select items from the other. There is no mechanism in the editor to ensure that these two arrays are synchronized, which means you must take on that responsibility. Additionally, the arrays can be quite lengthy from a data entry perspective, and the standard plist editor will quickly become an annoyance rather than helpful when editing these arrays. Thankfully it is quite easy to switch from one editor to the other from within XCode. If you need to switch editors, save your work, then right click on Root.plist in the project navigator and select Open As from the popup menu. Choose an appropriate editor and resume your editing. If you use the source (or text) editor you must ensure everything you type conforms to the XML specification or you could break the file, so be careful. You can also open the file outside of XCode, and use whatever text or XML editor you wish at that point.

Nicolaas tenBroek (AMDG)

307

iOS Development

Preferences and Settings

In the interest of being able to compare the different approaches we shall make our SettingsDemo app store and retrieve the same data we used in our previous examples. The first thing we need to do is remove all the example items from the PreferenceSpecifiers array. Move your cursor over each item in the array and press the minus icon. Doing so will remove the entire dictionary, so deleting them will not take much effort. When the array is empty we can add back in the two items we need for our app. Move your mouse over PreferenceSpecifiers and press the plus icon. We need two elements, so you can add one now and the second later, or add both now. It will not matter which order you use. The default data type for new items is String, but we need Dictionaries. Next to the word String you will notice an icon containing up and down arrows that indicate that this item is selected from a list. Click anywhere within that cell and you will see a popup menu containing all the valid types for that element. Change the type to Dictionary. After changing the type you will note that the Key (Item 0) now has a disclosure indicator next to it. If you select the key you will also see the standard plus and minus icons. Before pressing plus, you must change the disclosure indicator. If the indicator is pointing to the right (indicating that the tree has been collapsed), pressing the plus icon will add a new element to the PreferenceSpecifiers array. If the indicator is pointing down (indicating that the tree has been expanded), pressing the plus will add an element to the dictionary. Elements will always be displayed indented under their parent, so a quick visual inspection is all you need to know whether or not the new items are correctly placed. At this point you will need to create one text entry for the user and one slider for the value. When entering the Key attribute you must be very careful. This key must match the keys we use in our code exactly. Unfortunately, the keys will exist in at least two locations (the plist and at least one .m file), so all care must be taken when entering the terms. It also means you must test each setting whenever the files are changed to ensure that data is being stored and retrieved properly. Here is what our plist looks like after removing the default entries and adding in the two that we need:

Once the plist has been set up (and saved!) we can move on to our code. If you wish, you can run your program right now and see the settings. After running the app, simply press the home key and then open the settings app. You should see something like this in the simulator:

Nicolaas tenBroek (AMDG)

308

iOS Development

Preferences and Settings

If you press SettingsDemo you will see the following screen:

The code will be very similar to the template we used before, with small but very significant changes. We will begin by demonstrating another approach to handling the keys. In the previous example we created properties in the App Delegate to provide access to the keys. This time we will create external constant references to take the place of the properties. Creating an external constant requires two Nicolaas tenBroek (AMDG) 309

iOS Development

Preferences and Settings

steps, but they are a bit different from the steps needed for properties. As we examine the app delegates header file we see the references defined. Note that we used all upper-case letters for the names which is a coding convention used to designate constants. Remember that centralising the location of the keys will save you a great deal of work later, so this is an important step. The other difference in this header from the previous example is that there are no methods for accessing the preferences. The NSUserDefaults singleton will handle all of the access, so there is no need for us to provide anything else in the delegates header. AppDelegate.h
#import <UIKit/UIKit.h> @class ViewController; @interface AppDelegate : UIResponder <UIApplicationDelegate> extern NSString * const USER_KEY; extern NSString * const VALUE_KEY; @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) ViewController *viewController; @end

Our app delegate code is much reduced from the previous examples. We begin by establishing the values for our constants. These strings must match the ones we used in the plist exactly, so this is a point of failure in the process and the strings must be verified manually. A copy and paste operation would be well considered here. While provided, in this example the application:didFinishLaunchingWithOptions: method has not been modified from the template. Notice however that we did need to add applicationDidEnterBackground: and applicationDidBecomeActive:. Both of those methods contain the same single line of code. That code simply asks the user defaults singleton to synchronise itself. While the user defaults synchronises regularly on a set schedule, it is possible that the app is being suspended before that scheduled event will happen. It is also possible that the settings changed while the app was suspended and the scheduled synchronise will not happen before the settings are accessed again. So in both instances we merely ask that the process take place at an appropriate time. We did leave applicationWillTerminate: in the code just as a reminder. If you are writing an app that may be run on versions of iOS 1.x through 3.x, you must use applicationWillTerminate: rather than applicationDidEnterBackground: and applicationDidBecomeActive:. If your app will support all versions of the OS, then you must write all three methods. The method applicationDidBecomeActive: includes one other statement after synchronizing the user defaults. That line asks the view controller to re-load the preferences. The reasoning behind this step may not be obvious at first. In fact, the only time you will see the results of this step are when the app

Nicolaas tenBroek (AMDG)

310

iOS Development

Preferences and Settings

has been suspended, the settings changed, and the app restarted. This sequence of steps will not happen often, but it is important to both handle and test. AppDelegate.m
#import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate NSString *const USER_KEY = @"user"; NSString *const VALUE_KEY = @"value"; @synthesize window = _window; @synthesize viewController = _viewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES; } - (void)applicationDidEnterBackground:(UIApplication *)application { [[NSUserDefaults standardUserDefaults] synchronize]; } - (void)applicationDidBecomeActive:(UIApplication *)application { [[NSUserDefaults standardUserDefaults] synchronize]; [self.viewController loadPreferences]; } - (void)applicationWillTerminate:(UIApplication *)application { //used only for iOS versions before 4.0 [[NSUserDefaults standardUserDefaults] synchronize]; } @end

All that remains at this point is to demonstrate retrieval of the settings. Doing so is a fairly straightforward matter, simply get the NSUserDefaults singleton, and ask for the data you need. Of course, you can also store new settings if you have some need of that. Most often with the settings bundle approach you will not need to store new settings, as you would not regularly provide the user controls that modify the settings. In the interest of instruction though, we will provide an example that both reads and writes those values. To do so we will recreate the view that was used in both of the previous examples. Our view will contain a text field and a slider that will simply mimic the data you saw in the settings app.

Nicolaas tenBroek (AMDG)

311

iOS Development ViewController.h


#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) IBOutlet UITextField *textField; @property (nonatomic, weak) IBOutlet UISlider *slider; - (IBAction)sliderValueChanged; - (void)loadPreferences; @end

Preferences and Settings

Much of the implementation code is the same as we had in the previous examples. We still save the data when it is changed and load the data in viewDidLoad, though we moved the actual loading to the new method loadPreferences so that we could also call it from the App Delegate. The main difference is that now we access NSUserDefaults to access the data from the settings. The other difference from the previous example is the way in which we are accessing the keys. Notice that in the code below we simply use the name of the key references whenever we need them. This is possible because we imported the App Delegates header file. That import makes the externalised variables available to us just as if they were locally defined. ViewController.m
#import "ViewController.h" #import "AppDelegate.h" @implementation ViewController @synthesize textField; @synthesize slider; #pragma mark - TextField Delegate - (BOOL)textFieldShouldReturn:(UITextField *)theTextField { [textField resignFirstResponder]; [[NSUserDefaults standardUserDefaults] setObject:textField.text forKey:USER_KEY]; return YES; } #pragma mark - IBActions - (IBAction)sliderValueChanged { [[NSUserDefaults standardUserDefaults] setFloat:slider.value forKey:VALUE_KEY]; } #pragma mark - Preferences - (void)loadPreferences {

Nicolaas tenBroek (AMDG)

312

iOS Development ViewController.m


NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; textField.text = [userDefaults stringForKey:USER_KEY]; [slider setValue:[userDefaults floatForKey:VALUE_KEY] animated:NO];

Preferences and Settings

#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; } @end [self loadPreferences];

And there you have it! With hardly any effort we have demonstrated a powerful and yet easy-toimplement process. A quick save and run is all that is left to do. In this chapter we have examined two completely different approaches to handling user preferences and settings. Regardless of the approach you take, adding the ability for users to customise and personalise your app will have a big impact. Most users prefer apps that remember them, and modifying settings often helps users feel more invested in an app, meaning they will use it more and value it more as well.

Nicolaas tenBroek (AMDG)

313

iOS Development

Preferences and Settings

Nicolaas tenBroek (AMDG)

314

iOS Development

Animation

Animation
Animation may seem a frivolous addition to most any application, but it is actually a key component and should be considered for inclusion in everything you write be it a mobile app or desktop application. We are not necessarily speaking about game-style animation here, though that can easily be appropriate in several application categories. Rather, the kinds of animations that should be considered are informative ones. The old model of application programming was to have the application perform some tasks and wait for the user to figure out that the environment had somehow changed. Often information about the changes was displayed in status bars or by slight colour changes to icons. Of course, mobile apps cannot really spare the screen space for such items, and there is a growing doubt in the industry about whether or not such small changes are even detected by regular users (i.e. not programmers, we notice everything after all). The mobile app model requires lots of changes to the methods of application development, and animation gives us a very subtle but effective tool set for communicating with users in mobile environments. Of course, most applications should be completely functional independent of any animation. In other words, they should work nominally and meet the design specifications without the animation. Adding animation also means adding code that does not necessarily move us toward our goal, and every programmer knows that unnecessary code should be avoided at all costs because it is a breeding ground for bugs. So why bother to include animation? Is it really all that important? There are two big reasons why animation is a critically important consideration when designing an application. First, animation can inform the user about what is happening within the app. This information will be taken in almost unconsciously and will help inform the user about how to navigate the various areas of the app. In other words, animation can accomplish what status bars and icon changes attempt to do in desktop applications. For instance, imagine an app where tapping a cell causes a new view to be displayed. You could easily accomplish that activity without any animation at all. Tap the screen and in response, a completely new view is instantly displayed. That approach is entirely functional. Unfortunately, your average user will be temporarily bewildered about how to get back to the previous view. For them the experience is literally I tapped the screen and something happened. Of course, you will have a back button on the navigation bar, but the user sees a button that is pointing off to the side for some unknown reason. That pointing will cause the user to wonder about what is being indicated. There is no clear connection between that pointing symbol and what happened visually. Now imagine that same application with the slide-in animation that a navigation-based-application employs. The end result is the same as the previous incarnation in that the new view is displayed, but now we have informed the user about what took place. Specifically we showed them that the table and all its information continues to exist, but we are placing this new item of interest on top of the original screen. They also instinctively know that the action is reversible. Without consciously thinking about it, they are aware that there is a way to slide that new view back off the screen and see their previous display. A button on the navigation bar pointing in the opposite direction of the sliding action plays into that theme. It points in the opposite direction of the animation to clearly and quickly communicate the reverse action to the user. As an added benefit, all that communication happens non-verbally and is instantly understandable to all users. It needs no translation into additional languages or explanation,

Nicolaas tenBroek (AMDG)

315

iOS Development

Animation

and the animation accomplishes that communication without taking space away from your screen design. The second reason why you should consider adding animation to your application is that it will make your app appear to be more polished. It can quickly move your app from the realm of this little project I am working on to professional application worth paying for. You may think it trivial, but the fit and finish is probably the largest item setting professional applications apart from amateur projects. Most companies leave the fit and finish steps for late in the development process, whereas Apple is known for handling those items first. If you want your application to look like it belongs on an iOS device, you will need to adopt Apples approach. The effect of the placement of both animation and look and feel in the development process cannot be overstated. If you take those items into consideration early in the design process, they will become completely integrated with your design and form a cohesive whole. If you wait until the app is completely (or even nearly) functional before deciding about animation and look and feel, those items will end up appearing and feeling tacked on, awkward, and out of place. Worse, it might not happen at all. The moment an app is functional there will be tremendous pressure to get it out to the users as fast as possible. Whether the app is being developed by a corporate entity or by individuals there will be a tremendous investment in time, energy, and capital in its development. Those investments will create the pressures to get the app out to users, and those forces will not be swayed by the desire to make things pretty. The evidence for this process is all around us; simply take a few minutes to look through the app stores (check out several platforms). You will find plenty of examples of completely functional but ugly apps. You will also find beautiful versions of the same fundamental apps. The beautiful ones look much more professional than the ugly ones, and ironically they have often been created by individuals rather than corporations. Corporations typically take the function before design approach, thinking that the time spent on design is simply wasted money. That approach has led to us being surrounded by ugly functional applications every day in our desktop environments. In fact, those ugly applications have driven the development of ever more complicated means of interacting with the desktop as we constantly try to add some new mode on top of the existing system, in a desperate attempt to communicate the very kinds of information that beautiful animated applications convey with no apparent effort. The complicated and ugly approach simply will not work in the mobile world. In the mobile world screens are shrinking drastically and buttons are vanishing (hopefully for good). The only option we have is to put beauty back into our designs, and animation is one of the key ways to achieve that goal. With all that being said, you do need to be careful about the animation you add to any application. There are some very important guidelines to follow. Whatever animation you choose it should be consistent, informative, realistic, unobtrusive, fast, and smooth. Consistency is incredibly important here and something that is easy for programmers to overlook because we tend to break down our worlds into functionally discrete units. Users need to know what to expect from your animation for the same reasons they need to know what to expect from your UI. They should not be surprised by a sudden change in the animation when moving into a different area of the application. Consistent use of an animation idiom throughout the application can draw it together giving it a professional cohesive

Nicolaas tenBroek (AMDG)

316

iOS Development

Animation

feel. Inconsistent animation can make it look as if your application was built by pulling random parts from the bin with no thought whatsoever as to whether they belonged in the same app. That lack of planning makes an application feel at best amateurish and at worst, an assault on the senses. Inconsistent animation also violates the informative and unobtrusive guidelines. The only way to ensure that a consistent idiom is applied throughout the app is to design the animation at the very beginning. That way you will not create screens or controls that are incompatible with that animation later. All animation should serve to inform the user in some way. The visual changes must convey information to the user helping them understand what the application is doing at any given moment. If the user pinches and your app zooms in or out, that zoom process should be animated. The user will see the change in progress, match it to their actions, and fully understand why things became larger or smaller. Without the zooming action, the user will suddenly see that the display has changed, but may wonder if something else caused the change. A sudden change provides no information to directly link a users action with consequences. You should also avoid including animation that does not inform the user. If something starts animating for no apparent reason, or worse, provides an animation that is inconsistent with the action (e.g. sliding or flipping in response to a pinch rather than zooming), the user will be left confused, frustrated, and annoyed. The requirements for animations to be both realistic and unobtrusive cannot be overstated. Remember that the main purpose of the animation is simply to help the user understand what the application is doing. The animation is not the goal of the application. The user should be able to take in the information provided by the animation without expending additional cognitive effort. In fact, it needs to appear as if it is a natural and real-world consequence of their action. Your users are people; they interact with the natural world every day, and in the real world a persons action causes reactions (its Newtons third law!). If I push a childs ball off my desk it rolls and then bounces away. If I press a button on my keyboard, it moves down in response. Because these actions are both expected and unobtrusive, I am not amazed by them and do not need expend cognitive effort pondering why they are happening. The fact that they do happen means that my expectations about the world have been met and I am free to move on to the next task. If something else happens, say the ball shot straight up in the air when I pushed it off my desk, then some serious extra cognitive effort would be required on my part, and I would not be able to proceed to my next task (which is why I pushed the ball off my desk in the first place). Finally, the animations need to be both fast and smooth. The smooth action helps it appear more natural. When you drop a ball, it does not instantly begin to move at its terminal velocity, nor does it instantly stop when it reaches its destination. In fact, there is a gradual build-up of speed at the beginning, and then another change when it reaches the floor (it deforms, and then bounces back, though not as high). Animations that ignore realistic physics look unreal. They are also jarring to the user which then violates the unobtrusive requirement. The smoothness of animation is often achieved by having its speed change along a curve, just as real objects behave. While we are on the topic of speed, your animations need to be as fast as possible without violating the other requirements. Users do not want to wait for your animation to complete; they want to work on the next task. Think of pressing a button on your keyboard. If it took two seconds for each button to depress after you hit it,

Nicolaas tenBroek (AMDG)

317

iOS Development

Animation

you would quickly grow frustrated and change to a different keyboard. Keep that in mind. Your users are using the application to do something other than watch your pretty animations. You need to ensure that the user never feels like they are waiting for the animation to finish before they start working again. So, with all those requirements, how can you know if you have done a good job? The simple test is to give your application to other people and ask them to try it out. Dont tell them what you are looking for, but watch them give it a go. If they appear nonplussed by animations and confident in what they need to do, then you have done a great job. Conversely, if their response is: Wow! Thats a really cool animation! then you have not. Remember that for the most part, no one notices when real world objects behave the way they are supposed to. Your goal is to have your application fit the users expectations of how things work.

Nicolaas tenBroek (AMDG)

318

iOS Development

Animation

Animation on IOS
IOS provides multiple approaches for implementing animation, each with its own set of strengths and weaknesses. The first approach is a set of animations you always have access to, which I will classify as UIView Animations for lack of a better term. The second approach is Core Animation, and the third is OpenGL. In implementation, OpenGL is housed under Core Animation, but it is its own topic. In fact, OpenGL is such a massive topic that it is well beyond the scope of this book. OpenGL is an entire graphics system typically used for animating game and simulation apps. In contrast, UIView Animations and Core Animations deal primarily with animating standard iOS graphical objects. They are both far more accessible than OpenGL and are the options the typical application will turn to for animation.

UIView Animation
UIView Animation can be used for easily animating transitions between views, or for animating objects within a view. While the UIViews abilities are somewhat limited, it is almost impossibly easy to use, and gives fast access to the basic animations a typical application will often need. UIView Animations are classified as implicit animations because the programmer needs only to specify the ending state and the duration of the animation. All of the work of animating the change from the views current state to its ending state is handled automatically by the view. The majority of the methods you will need to use for UIView Animations are housed within the UIView class (which admittedly may have already been apparent). As virtually all of the graphical objects extend UIView, having the methods housed in that class means that nearly all of the graphical objects can be individually animated. While almost all objects can be animated, these first two methods we will examine are designed explicitly for animating the transition between one view and another, replacing the entire screen. While the methods themselves seem fairly generic, there are only a limited number of pre-built transitions available. In fact, if you do not count none as an option, there are only three unique transitions. Most of the options are actually the same animation travelling in a different direction. The options are stored in the UIViewAnimationOptions constants and are: flip in from the left; flip in from the right; flip in from the top; flip in from the bottom; curl up; curl down; and cross dissolve. The flip animations produce an effect of showing the user the back of a view. This is often used for revealing settings within an app. The curling transitions are also often used for settings, but do not hide the main view entirely. The effect produced is as if the corner of the view were lifted to reveal something underneath. It treats the view as a sheet of paper and curls it upwards. UIViewAnimationOptions is really an enum, so it also contains other options to change the way an animation appears. When methods use it as an argument, the argument is a mask of the options typically combined with a bitwise OR operation. The first method, transitionFromView:toView:duration:option:completion is for animating the transition between two separate views. The first two arguments are pretty obvious; they are the current view (usually just self), and the new view to be displayed. The duration argument is an NSTimeInterval (really a float) representing the number of seconds the transition will take. Keeping in mind the requirements for all animations we listed earlier, this number should generally be far less than one second. Remember that we do not want the user waiting for our animation to complete. The

Nicolaas tenBroek (AMDG)

319

iOS Development

Animation

option argument is one of the four UIViewAnimationOptions. The completion argument is a code block that will be executed when the animation has run to completion. The second method, transitionWithView:duration:options:animations:completion is for faking an view transition. It gives you the opportunity to modify the view in some way and present it back to the user as if it were a new view. The arguments are the same as the previous method except for the lack of a toView. This method may be the only place where the none option will come in handy. Rather than having the current view moving around and coming back, this method can smooth changes within a single view (i.e. a new control appearing or an old one disappearing, or a large data change). Customised view animations that are not transitions can also be handled through UIView, but require a bit more effort on your part. While UIView contains a large number of animation related methods, most of them are old and their use has been discouraged for any app designed to run in iOS version 4.0 or later. The old methods have all been replaced with three newer methods: animateWithDuration:animations:, animateWithDuration:animations:completion:, and animateWithDuration:delay:options:animations:completion:. Obviously, all three of these methods do the exact same thing. The first two are simply convenience methods for use when you do not need all the arguments. As before, the duration argument is an NSTimeInterval specifying the duration of the animation in seconds. The animations argument is a code block containing all the changes you want animated. The completion argument is a code block to be run when the animation completes. The options argument is a mask of UIViewAnimationOptions, and the delay is an NSTimeInterval specifying the number of seconds to wait before beginning the animations. The first two of these methods do not support a delay, so your animations are immediately started. All UIView Animations run in a separate thread, so they will not block the applications core functionality, but there is only a single thread available to handle all the views animations. All UIView objects contain a set of properties that control the size and position of the view on the screen. Changes to any of these properties can be animated. For instance, if you would like a view to slide on the screen from the top down rather than from the side like the standard navigation app, you could animate the centre property to accomplish that. The properties available through the UIView are: bounds, centre, and frame. The centre property is a CGPoint. CGPoint is a C structure containing two floats, one called x and one called y (we will discuss structures in a bit). This point is a location within the superviews coordinate system, so any change to it will move the view in relation to its superview. The size of a view is controlled through both the bounds and frame properties, which are both items of type CGRect. CGRect is a C structure that contains two other structures, one a CGPoint called origin, and the other a CGSize called size. The CGSize structure contains two floats, one called height and one called width. While it may seem like unnecessary duplication to have two separate rectangles here, there is purpose in the apparent madness. The rectangle called frame is expressed in the superviews coordinate system (which might be pixels or something else), whereas the bounds rectangle is expressed in a coordinate system unique to the view itself and is relative to its frame. This means that the origin value of the frame property tells you the location of the item relative to its superview, whereas the default origin value of the bounds property is always (0, 0), or in other words, the bounds

Nicolaas tenBroek (AMDG)

320

iOS Development

Animation

begins at the top-left corner of the frame. The bounds extend from (0.0, 0.0) to (1.0, 1.0) regardless of the coordinate system used by the frame. The size of the rectangle is always the same for both bounds and frame, and any change to size of one property also changes the size of the other property. So, if you want to change the size of a view on the screen you could modify the size of the bounds property. Any change to size in the bounds property is automatically reflected in the frames size as well. Similarly, any change to the centre property is automatically reflected in the frames origin property (but not the bounds origin which is always: 0, 0). The relationship between the frame and the bounds is a complex one. When doing simple animations it really does not matter which one you use. However, the frame can only be changed directly if the transform property has not been set, and most of the more complex animations will set this property. So, if you notice that modifications to the frame appear to have no effect, try modifying the bounds instead. Lets take a look at two UIView animation examples. The first example animates a delete operation by having the sub-view both shrink and fade away into nothing. At the end of the animation the now invisible sub-view is removed from its super-view to complete the deletion. UIView Animation Example I
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionCurveEaseIn animations:^{ CGRect frame = subView.frame; frame.origin.x += frame.size.width / 2; frame.origin.y += frame.size.height / 2; frame.size.height = 0; frame.size.width = 0; subView.frame = frame; subView.alpha = 0.0; } completion:^(BOOL completed){ [subView removeFromSuperview]; } ];

Notice we begin the process by creating a copy of the frame, and then all modifications are made to the copy. Finally, the copy is assigned over the original frame property. The way the C structures are handled is a bit funny. The internal variables act as if they are read-only within the view object. The only change allowed is to replace the entire structure. A C structure is an old-fashioned mechanism for combining multiple primitives into a single item. That item is not quite an object as it cannot contain methods, and the placement of the variables in RAM never changes. There really is no difference between creating a structure and individually declaring each of the variables. The structure is simply a convenient method for declaring and manipulating multiple variables at one time. So the statement CGRect frame = subView.frame; is simply creating

Nicolaas tenBroek (AMDG)

321

iOS Development

Animation

a new structure and then copying the values from the sub views structure into the new one. Once the copy has been made we can modify it to our hearts content, and then replace the entire structure when we are done. The UIView Animation process will track the properties being changed and animate those changes. For this example we modified the frame and the alpha properties, so those two properties will be animated. In the ordinary course of events the view will see a property being changed, calculate the distance between the original property values and the new ones, and distribute that distance smoothly over the duration requested. However, in this case we used an option to change the distribution of the modifications. We used the curve ease in option, which means animations will begin at a slower pace, and then speed up. Once the maximum speed has been reached the remaining distance will be distributed smoothly over the remaining time. Modifying the distribution of change like this can make our animations look like they are obeying the physical laws of the real world. There are four options for distributing the changes across the distance of an animation: curve ease in, curve ease out, curve ease in out, and curve linear. Use them appropriately and they will go a long way toward making your animations look more realistic. The second example animates an image dropping from its current location on the screen to the bottom of the screen, and then bouncing back up to its starting point. Note that the starting point is saved in a property for re-use at the end of the animation cycle. UIView Animation Example II
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionAutoreverse animations:^{ self.startingPoint = imageView.center; CGPoint centre = imageView.center; centre.y = self.view.bounds.size.height - imageView.bounds.size.height / 2; imageView.center = centre; } completion:^(BOOL completed){ imageView.center = self.startingPoint; }

];

You likely noticed that we used two options this time, combined with a bitwise OR operation. The auto reverse option causes an animation to be run again in reverse after it has completed. Oddly, this option does not actually move the final location of the view; instead it simply records its animations and runs them again in reverse. If we had failed to move the view back to the starting point in the completion block, then after the animation completed, we would see the view suddenly jump from the top of the screen to the bottom. By moving the view in the completion block it will simply appear to stop moving at the top of the screen.

Nicolaas tenBroek (AMDG)

322

iOS Development

Animation

Core Animation
A full treatment of Core Animation is beyond the scope of this book, but a good portion of it is easily accessible and can be used to great effect with minimal effort. We will focus on the CAAnimation class and its subclasses, as well as an introduction to the CALayer class. For an in-depth treatment see Apples Core Animation Programming Guide, or any number of books that have been dedicated to the topic. Unlike OpenGL (which is essentially platform independent), much of Core Animation will be familiar to you. In fact, every UIView contains a CALayer and the UIView uses its CALayer for much of its display, so you have already seen its effects and have been indirectly exposed to some of its properties. This familiarity makes the code more approachable for non-animation professionals, and makes its animations a good mid-point between the basic UIVew and the complex OpenGL. The basic idea behind Core Animation is that your view is a 2 dimensional plane existing in a 3 dimensional space. That plane is called a layer and is represented by the class CALayer. In some ways the idea of a layer is similar to the idea of a view. For instance, just as we can create a complex view (or control) by combining simpler views, we can create a complex layer by combining simpler layers. Any modification to a layer will automatically propagate down the tree of sub layers. This approach leads to code that is both more organised and easily modifiable whether you are dealing with a view object or a layer object. Additionally, the CALayer has the same bounds and frame properties that the UIView has, and those items share the same relationship as they do in the UIView. Of course, there are also some major differences between the UIView and the CALayer that are important to for you to know. One of the main differences between the UIView and the CALayer is that the CALayer is Key-Value coding compliant like an NSDictionary. This means that in addition to directly accessing properties through the dot operator, you may also use the keyPath methods valueForKeyPath: and setValue:forKeyPath:. The methods are the preferred mechanism for accessing properties when creating animations. In fact, for some of the values, the keyPath methods are the only mechanism available. For instance, if you wanted to access the x-axis rotation of a layer called imageLayer you could not simply write: CGFloat xRotation = imageLayer.transform.rotation.x; Instead you would need: NSNumber *xRotation = [imageLayer valueForKeyPath:@transform.rotation.x]; Another difference is that the CALayer holds a 4 x 4 matrix of values that describe the relative position of the layer in space, any of those values may be modified to move the layer within the space. The matrix may be modified to affect rotation, scale, translation, skew, and projection of the layer in space. The matrix itself is contained within a C structure called CATransform3D. You may modify the CATransform3D directly through the layers transform property; use the key-path methods to access part of it; or use the CATransform3DXXXXX methods to produce an entirely new matrix which can then be applied to the layer, replacing the current one. There are CATransform3D methods to modify the scale of the layer, rotate and invert the layer, concatenate two matrices, and make an affine transform. Nicolaas tenBroek (AMDG) 323

iOS Development

Animation

You can even directly access the matrix if so you so desire, and produce transforms as complex as you can imagine. If you take that route, be sure to send a thank you e-mail to your Discrete Maths instructor after you have finished. The elements inside of CATransform3D have all been given terribly unhelpful names like m11, m12, and m43. However, if you use the key-path methods to access the transform, you can use the much more descriptive names rotation, scale, and translation (there is no naming scheme that allows access to the skew). Each named-path provides an x, y, and z axis value. Additionally and somewhat confusingly, each key-path name can be used on its own without a factor. For instance, using the path @transform.rotation yields the same result as @transform.rotation.z, while @transform.scale yields an average of the three scale factors. Using @transform.translation yields a CGSize containing the x and y factors. The CALayers properties are a bit different from those in UIView as well, and these differences give you some additional animation options. The centre property of the UIView is replaced with position in the CALayer, and is augmented by two new properties called anchorPoint and anchorPointZ respectively. The property anchorPoint gives a location on the X and Y-axes and is a CGPoint. The anchorPointZ gives a location on the Z-axis and is a CGFloat. The anchor point is expressed in the bounds coordinate system and defaults to (0.5, 0.5), whereas the position is expressed in the super views coordinate system. The anchor points are used as a relative positioning when applying transformations to a layer. For instance, if you rotate a frame, the anchor point acts like a pin with the frame rotating around it. When the anchor point is in the centre of the layer (its default position), the frame stays in the same relative position in its super view and spins on its axis. If the anchor point is in one of the frames corners or edges, then the rotation is around that position, and the effect will be the layer moving along an arc. In addition to size, rotation, and position, CALayer allows animation of opacity (so a view can fade in or out), hiding and showing, background and border colours, the width of the border, scaling, shadows, and even the contents of the layer itself.

Nicolaas tenBroek (AMDG)

324

iOS Development

Animation

CAAnimation While the CALayer holds all the data for the layout and display of your view, actually animating that display requires some additional classes. CAAnimation is the base for those animations, though the class itself is not terribly helpful as it is abstract. CAAnimation gives us three useful properties: delegate, removedOnCompletion, and timingFunction. The delegate provides access to two methods: animationDidStart: and animationDidStop:finished:. The removedOnCompletion property can be set so that the animation is removed from the layer after completion. This is a handy property whose use can often obviate the need for a delegate. The timingFunction controls whether the animation is linear, eases in, eases out, or eases both in and out. In addition to those basic properties, CAAnimation implements the CAMediaTiming protocol, which adds properties for controlling the starting time, duration, speed, repeat count, and auto-reverse ability. CAAnimations children: CAAnimationGroup, CATransition, and the CAPropertyAnimation family are where the real work of animating happens. We will look at each of these in turn, but we will start with the simplest: CAAnimationGroup. CAAnimationGroups job is to group together a set of animations for management purposes. You can accomplish the exact same animations without creating an animation group as you can with one, but grouping the animations together will condense the management of those animations and result in less code for you to write. The group is made up of an array of CAAnimation instances called animations and has no methods other than the set and get property methods. Below is an example of managing several animations with an animation group. The details of creating the animations themselves will be left for later examples, as they are not important right now. In this example, three different animations are applied to a single sub-view at the same time. CAAnimation Example I
CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc] init]; animationGroup.animations = [NSArray arrayWithObjects:sizeAnimation, pulseAnimation, moveAnimation, nil]; animationGroup.duration = 1.0; animationGroup.repeatCount = HUGE_VALF; animationGroup.autoreverses = YES; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; [subView.layer addAnimation:animationGroup forKey:@"goCrazy"]; //If you are not using ARC, you are responsible for releasing the animation group when you are done with it.

Note that unlike the built-in animations, the libraries for Core Animation are not included in your application by default. To gain access to these libraries you will need to add the QuartzCore framework to your application, and then either #include <QuartzCore/QuartzCore.h> or #include <QuartzCore/CoreAnimation.h> in your .m files.

Nicolaas tenBroek (AMDG)

325

iOS Development

Animation

CATransition CATransition is a class housing the common animations used to transition from one view to the next. While the iOS version of this class is much more limited than the MacOS version, it does have some very useful transitions which can be used to make your application behave exactly like the built-in iOS applications. The transitions supplied by iOS are fading out, reveal, slide in, and push in. If you use slide in the new view moves in over the top of the existing view, which remains stationary. If you use push in the new view pushes the old one out of the way, so both views are animated. With the exception of fade, the transitions can happen from the top, bottom, left, or right side of the screen. CATransition is another example of implicit animation. While you have a good deal of control over its behaviour, you are not responsible for specifying where the views are at any given point. Here is a basic example of animating the transition between two different views. Each view contains a UIButton that is used to initiate the transition to the other view. View Controller One
- (IBAction)go { CATransition *transition = [CATransition animation]; transition.delegate = self; transition.duration = 1.0; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromTop; transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; [self.view.layer addAnimation:transition forKey:kCATransitionPush]; } - (void)animationDidStart:(CAAnimation *)theAnimation { ViewTwo *viewTwo = [[ViewTwo alloc] initWithNibName:@"ViewTwo" bundle:nil]; [self.view addSubview:viewTwo.view]; }

Note that unlike the built-in animations, the libraries for Core Animation are not included in your application by default. To gain access to these libraries you will need to add the QuartzCore framework to your application, and then either #include <QuartzCore/QuartzCore.h> or #include <QuartzCore/CoreAnimation.h> in your .m files. View Controller Two
- (IBAction)goBack { CATransition *transition = [CATransition animation]; transition.delegate = self; transition.duration = 1.0; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromBottom; transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; [self.view.layer addAnimation:transition forKey:kCATransitionPush]; } - (void)animationDidStart:(CAAnimation *)theAnimation {

Nicolaas tenBroek (AMDG)

326

iOS Development View Controller Two


self.view.hidden = YES; } - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag { [self.view removeFromSuperview]; //be sure this view controller is released at some point if not using ARC }

Animation

Note that by alternating the animation directions (controller one pushes from the top and controller two pushes from the bottom) we maintain the spatial relationship of the two views for the user, making the animations more realistic. Custom UIButtons using arrows could enhance this effect and make our animations very informative.

Nicolaas tenBroek (AMDG)

327

iOS Development

Animation

CAPropertyAnimation While CAPropertyAnimation is abstract, it is the parent class of CABasicAnimation and CAKeyfameAnimation, so it is worth a quick examination. The property-type animations are designed to handle all the properties from CALayer which are marked in the documentation as animatable. You will recall that each of those properties can be accessed through the key path methods. The CAPropertyAnimation and its subclasses use those methods directly, so one of the most important properties is the keyPath property. The keyPath property is an NSString reference that holds the name of the property you wish to animate. This is initially set through the animationWithKeyPath: class method that is used to create a property animation instance. The other items of note in this class are the properties: cumulative, additive, and valueFunction. You will recall that when we used UIView animations, we simply specified the ending point of the property values and the view calculated an animation to get to that point. With CALayer properties, we can specify each point along the animation path, and we can then use these three properties to specify how the animations should affect the values of the properties being animated. For instance, if you set cumulative to YES, then the value of the property at the end of each animation repeat-cycle is added to the current property value. If additive is set to YES, then the new value specified in the animation is set to the propertys value. The valueFunction property allows you to specify how the new values will be integrated with the old.

Nicolaas tenBroek (AMDG)

328

iOS Development

Animation

CABasicAnimation The CABasicAnimation class is most useful for animating properties that do not involve moving the layer, or for very simple (linear) moves. The animations produced by this class are again implicit animations, though you have a little more control than with the previous implicit animation types as we can specify the mid-point of the animation. CABasicAnimation adds three properties to those inherited from CAPropertyAnimation: fromValue, byValue, and toValue. The animations produced modify the property along the lines produced by (fromValue + byValue) and (toValue - byValue). While each of the properties can be nil, you will generally specify at least one of them. If all three are nil, then the animation will be a transition between the propertys previous value and its current value. Setting either byValue or toValue produces an animation from the propertys current value to the specified value. Setting only fromValue produces an animation from fromValue to the propertys current value. Below are three examples of CABasicAnimations. The first example animates a sub-view fading out while remaining in place. This type of animation could be useful for animating the deletion of a graphical component. CABasicAnimation Example I
CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; fadeAnimation.toValue = [NSNumber numberWithFloat:0.0]; fadeAnimation.duration = 0.45; fadeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; fadeAnimation.removedOnCompletion = YES; [subView.layer addAnimation:fadeAnimation forKey:fadeAnimation.keyPath];

This next example also modifies the opacity, but this time we use it to create a pulsing effect. This kind of effect can be useful for subtly calling the users attention to something they need to do, or some component they should interact with. In this example we set the repeatCount to HUGE_VALF that causes the animation to repeat forever, and we enable auto-reverse, which causes the animation to reverse before starting over. CABasicAnimation Example II
CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; pulseAnimation.fromValue = [NSNumber numberWithFloat:1.0]; pulseAnimation.toValue = [NSNumber numberWithFloat:0.6]; pulseAnimation.duration = 1.0; pulseAnimation.repeatCount = HUGE_VALF; pulseAnimation.autoreverses = YES; pulseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; [subView.layer addAnimation:pulseAnimation forKey:pulseAnimation.keyPath];

In the final example we animate the size of the layer, giving it the appearance of growing in size, or approaching the user from a distance.

Nicolaas tenBroek (AMDG)

329

iOS Development CABasicAnimation Example III

Animation

CABasicAnimation *growAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size"]; CGSize size = subView.layer.bounds.size; growAnimation.toValue = [NSValue valueWithCGSize:size]; size.height = 0.0; size.width = 0.0; growAnimation.fromValue = [NSValue valueWithCGSize:size]; growAnimation.duration = 0.75; growAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; [subView.layer addAnimation:growAnimation forKey:growAnimation.keyPath];

Nicolaas tenBroek (AMDG)

330

iOS Development

Animation

CAKeyframeAnimation A key-frame animation is basically a set of points along a path to be animated. The points can be specified in an array, or as a CGPathRef. Using a key-frame allows us to break free of the strictly linear interpolations of the previous classes and allows us to specify as much of the animation path as we desire. The path can be virtually anything within the 3 dimensional spaces available to us. We can use either the path or values properties for defining the points (depending on whether we wish to use an array or a CGPathRef). Each segment of the path can be treated as its own separate animation in that we can define unique durations and timing functions for that segment using the keyTimes and timingFunctions properties. Key-frame animations can even auto rotate tangentially along the path if you so desire. Below are two key-frame animation examples. These are a bit longer than the other examples we have seen so far in this chapter, though they are not necessarily more complicated. We are simply including more steps in the animation, and each step contains animation code of the type we have already explored. The first example marches a sub-view around the bounds of its super-view. After completing the circuit the sub-view is returned to its starting position on the screen. CAKeyframeAnimation Example I
CGMutablePathRef path = CGPathCreateMutable(); NSMutableArray *timingFunctions = [[NSMutableArray alloc] init]; CGPoint position = subView.layer.position; CGFloat halfHeight = subView.bounds.size.height / 2; CGFloat halfWidth = subView.bounds.size.width / 2; //add initial point to path. //Some of the CGPath methods will not work on an empty path, //so this can be used to create a non-empty path with no movement CGPathMoveToPoint(path, NULL, position.x, position.y); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; //gentle move to top-left corner CGPathAddCurveToPoint (path, NULL, position.x, position.y, position.x + position.x / 2, position.y / 2, halfWidth, halfHeight); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; //move to bottom-left CGPathAddLineToPoint(path, NULL, halfWidth, self.view.frame.size.height - halfHeight); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; //move to bottom-right CGPathAddLineToPoint(path, NULL, self.view.frame.size.width - halfWidth, self.view.frame.size.height - halfHeight); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; //move to top-right

Nicolaas tenBroek (AMDG)

331

iOS Development CAKeyframeAnimation Example I

Animation

CGPathAddLineToPoint(path, NULL, self.view.frame.size.width - halfWidth, halfHeight); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; //move to top-left CGPathAddLineToPoint(path, NULL, halfWidth, halfHeight); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; //move back to starting point CGPathCloseSubpath(path); [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; animation.removedOnCompletion = YES; animation.path = path; animation.duration = 4.0; animation.calculationMode = kCAAnimationPaced; //setting the rotation mode to auto keeps the bottom of the view against the edge of the screen animation.rotationMode = kCAAnimationRotateAuto; animation.timingFunctions = timingFunctions; [subView.layer addAnimation:animation forKey:animation.keyPath]; [timingFunctions release], timingFunctions = nil; CGPathRelease(path);

The next animation is a bit more complex. Imagine an app that displays a number of images and we want to give the user the ability to interact with each image individually. Additionally, we decide that we want to create a novel way for the user to delete those images by flicking them off the screen. To make this work we created a gesture recogniser (not shown) that would recognise the flicking action, and then used that action to initiate the deletion. When flicked the image will tumble and fly off the screen in the direction of the flick. CAKeyframeAnimation Example II
#pragma mark #pragma mark handle touches - (void)handleFlickGesture:(FlickGestureRecogniser *)flickGestureRecogniser { switch(flickGestureRecogniser.state) { case UIGestureRecognizerStateBegan: startPoint = [flickGestureRecogniser locationInView:self.view]; break; case UIGestureRecognizerStateEnded: endPoint = [flickGestureRecogniser locationInView:self.view]; UIImageView *image = [self getImageAtPoint:startPoint]; if(image) { [self flickAnimateImage:image toPoint:[self predictedLastPoint]];

Nicolaas tenBroek (AMDG)

332

iOS Development CAKeyframeAnimation Example II


default: } } } break; return;

Animation

In this section we have the method called by the gesture recogniser. Gesture Recognisers are discussed in the chapter on sensing, so we will not be covering the details of that activity here. It will suffice to say that this method is called several times during the course of a gesture, though we are interested in only two of those events. We need to know when the gesture begins and when it ends, so that we can record points on the screen that can then be used to compute a line when the user flicks an image. Before beginning the animation we check to see whether or not the user actually flicked an image. The user may have flicked the empty space between images, and that gesture would need to be ignored. In this method we call two helper methods: getImageAtPoint: and predictedLastPoint which we will see later. Next, we will examine the animation process itself.

CAKeyframeAnimation Example II
#pragma mark #pragma mark animations - (void)flickAnimateImage:(UIImageView *)image toPoint:(CGPoint)lastPoint { CGPoint startingPoint = image.layer.position; double distance = sqrt(pow(startingPoint.x - lastPoint.x, 2) + pow(startingPoint.y - lastPoint.y, 2)); NSMutableArray *spins = [[NSMutableArray alloc] init]; CGFloat CGFloat CGFloat CGFloat angle = M_PI; x = 0.53f; y = 0.37f; z = 0.15f;

for(int i = 0; i < distance / image.frame.size.height + 1; i++) { [spins addObject:[NSValue valueWithCATransform3D: CATransform3DMakeRotation(angle, x, y, z)]]; x += 0.53f; y += 0.37f; z += 0.15f; angle += M_PI; } CAKeyframeAnimation *spinAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; spinAnimation.values = spins; [spins release], spins = nil;

Nicolaas tenBroek (AMDG)

333

iOS Development CAKeyframeAnimation Example II

Animation

CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; moveAnimation.toValue = [NSValue valueWithCGPoint:lastPoint]; NSArray *animations = [NSArray arrayWithObjects:spinAnimation, moveAnimation, nil]; CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc] init]; animationGroup.animations = animations; animationGroup.duration = 0.5f; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; animationGroup.removedOnCompletion = YES; [animationGroup setValue:image forKey:FLICK_NAME]; animationGroup.delegate = self; [image.layer addAnimation:animationGroup forKey:FLICK_NAME]; } - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag { [[theAnimation valueForKey:FLICK_NAME] removeFromSuperview]; }

The method flickAnimateImage:toPoint: has quite a bit of activity, and is completely undocumented for some reason, so lets walk through it a bit at a time. We begin by calculating the distance the image will travel from its current position. Next we create enough rotations to ensure it will rotate along the entire path. If we had created a pre-determined number of rotations and then applied the rotations to every image the actual animations would be quite jarring. Images travelling a shorter distance would rotate quite a bit faster than those travelling a longer distance. By computing the number of rotations at run time we ensure that the animations will be consistent regardless of the distance the image will travel. Once the rotations have been calculated we create an animation group. This is similar to the process we used earlier. Notice that we used the ease out timing function. Think about flicking an object in the real world. When flicked the object takes off at a fairly rapid pace, and then slows down as it travels. We want our images to respond in a similar fashion. When the animation ends it is time to dispose of the image in animationDidStop:finished:. Notice we can use the passed in argument to find the image that was animating. This way we do not need to keep track of it elsewhere. Next we have our two helper methods. The first is getImageAtPoint: and is used to retrieve the image that the user is touching. If the user is not touching an image, then nil is returned. This is fairly standard code and can be used any place when you need to discover what the user is touching on the screen. The second method is predictedLastPoint. This method takes the two points we recorded earlier: the starting touch and the ending touch, and uses those to predict where the image would go if flicked. The method may seem long, but it is simply basic geometry, so there really isnt anything at all complicated in it.

Nicolaas tenBroek (AMDG)

334

iOS Development CAKeyframeAnimation Example II


#pragma mark #pragma mark helpers

Animation

- (UIImageView *)getImageAtPoint:(CGPoint)point { for (UIView *subView in [self.view subviews]) { if ([subView isMemberOfClass:[UIImageView class]] && CGRectContainsPoint([subView frame], point)) { return (UIImageView *)subView; } } return nil; } - (CGPoint)predictedLastPoint { CGPoint lastPoint; lastPoint.x = endPoint.x - startPoint.x; lastPoint.y = endPoint.y - startPoint.y; //check for horizontal movement if(lastPoint.y == 0) { if(lastPoint.x > 0) { lastPoint.x = self.view.bounds.size.width; } else { lastPoint.x = 0; } lastPoint.y = startPoint.y; return lastPoint; } //check for vertical movement if(lastPoint.x == 0) { if(lastPoint.y > 0) { lastPoint.y = self.view.bounds.size.height; } else { lastPoint.y = 0; } lastPoint.x = startPoint.x; return lastPoint; } CGFloat slope = lastPoint.y / lastPoint.x; //calculate two points on the line, and then use the one that is in the bounds of the screen CGPoint lastPointOne; if(lastPoint.x > 0) { //moving down lastPointOne.x = self.view.bounds.size.width; } else { //moving up lastPointOne.x = 0; } lastPointOne.y = slope * (lastPointOne.x - endPoint.x) + endPoint.y;

Nicolaas tenBroek (AMDG)

335

iOS Development CAKeyframeAnimation Example II


CGPoint lastPointTwo; if(lastPoint.y > 0) { //moving right lastPointTwo.y = self.view.bounds.size.height; } else { //moving left lastPointTwo.y = 0; } lastPointTwo.x = ((slope * endPoint.x) + (lastPointTwo.y - endPoint.y)) / slope; if(lastPointOne.y > -1 && lastPointOne.y <= self.view.bounds.size.height) { lastPoint = lastPointOne; } else { lastPoint = lastPointTwo; } } return lastPoint;

Animation

Last but not least, in the viewDidLoad method we set up the flick gesture recogniser and attach it to our view. This will kick off the whole animation process, so its pretty important. CAKeyframeAnimation Example II
#pragma mark - (void)viewDidLoad { [super viewDidLoad]; FlickGestureRecogniser *flickGesture = [[FlickGestureRecogniser alloc] initWithTarget:self action:@selector(handleFlickGesture:)]; [self.view addGestureRecognizer:flickGesture]; [flickGesture release], flickGesture = nil; }

With these tools you can now easily add animation into any app. While you will doubtless run into many situations where you need to add animation into an existing situation, it is always better to plan it in at the beginning of the app development process. Planning for animation allows you to ensure that whatever you design fits into the overall character of your app. Whatever you finally decide, you need to remember that each animation must be consistent with the other animations in the app, informative, realistic, unobtrusive, fast, and smooth. If you can keep to those suggestions, your animations will be quite successful in making your apps appear professional and be a pleasure to use. If you fail to keep those guidelines, your app runs the risk of seeming amateurish and dysfunctional, and no one wants to use an app like that.

Nicolaas tenBroek (AMDG)

336

iOS Development

Animation

Nicolaas tenBroek (AMDG)

337

iOS Development

Accessibility

Accessibility
The majority of readers are likely not aware of the iPhones support for accessibility but in fact, the iPhone has become quite popular with blind and visually impaired smartphone users. Several years have passed since the introduction of the iPhone, and still most people are surprised to learn of this. Their surprise is easily understood of course, because the iPhone has no control buttons. Sure, it has a button to lock the screen, a switch to mute the speaker, volume control, and the home button, but none of those buttons actually control apps. In fact, re-tasking those buttons as controls within an app is strictly forbidden by the app store guidelines. So how can a phone with nothing but a touch screen for control become the smartphone of choice among the blind and visually impaired? The answer is actually quite simple: Accessibility is built into the core of the system rather than being tacked on as an afterthought. The iOS platform offers tremendous support for accessibility features. It has a built-in multi-lingual screen reader (a feature that costs nearly $500 to add on to some other platforms), it provides support for inverted screen colours, large fonts in some built-in apps, and can even put the screen in a permanent zoom-able mode. Most of these features designed in such a way that in-app functionality is automated, and you do not need to do anything to your app in order to support them. For instance, inverting screen colours is accomplished through some simple maths and can be done without any apps knowledge or consent. Therefore apps do not really need to be written with that specific ability in mind (though it is often worth testing to ensure your app will continue to work well with that feature enabled). Supporting the screen reader is a much different prospect though, and requires that an app explicitly provide an interface. One of the biggest reasons for this explicit requirement is that programmers simply do not put all the instructions for using our apps into words; rather we rely on visual cues to deliver the vast majority of the instructions within our apps. We write apps that way because the human brain can process lots of different visual cues very quickly, much more quickly than we can glean information strictly from reading. For instance, consider the following screen grab from a fictitious app using a very common iOS design:

From a quick inspection we can see that this is a navigation-based app of some sort, and while we have no idea of the subject, we do know that pressing that little plus button will take us to a screen where we can add a new piece of data or widget that is appropriate to the app. How did we know that? There are no written instructions on the screen, but there are plenty of visual cues. For instance, you know that when you see a navigation bar above a table that this is a navigation-based app. You also know that the plus icon thing is a button because it appears to be sitting above the bar. The icon also quickly informs you of the nature of the button. Even the colour informs us. The button is blue, so its not likely to do something that is dangerous or irreversible (unlike a red button). But how can a screen reader possibly process all of that information and convey it to a blind user in a comprehensible manner? The simple

Nicolaas tenBroek (AMDG)

338

iOS Development

Accessibility

answer is of course that it cannot. So, lets try another approach. This time we will replace the plus icon with a word so the screen reader has something to read.

So, now the screen reader loudly proclaims Add to the user, which is hardly better. To better understand the issue at work here, try this thought experiment: Close your eyes and imagine someone yells Add! at you. Now, what did they mean by that? Was that a command for you to add something? Should you be doing arithmetic at the moment or adding a new ingredient to dinner? Did they mean, Press me and Ill add a widget? Without context there is no way to be sure of anything other than the fact that we really do not know what the command meant. All the visual cues we normally receive provide that context which then allows us to interact with our apps with little to no written instruction. Therefore, in order to make our apps more accessible to the blind and the visually impaired, we must work to provide the context verbally. Thankfully, this is actually quite easy to do, and in most circumstances requires no change to the design of our apps. In the vast majority of cases we will need only to provide the contextual and instructional information to the screen reader and things will continue on swimmingly. There will be occasions where designs might need to change, but those occasions will be few and far between. For instance, consider a situation where we have some words appear on the screen for a few moments and then disappear automatically (i.e. a non-essential notification about app activity, or game characters engaged in a dialogue). We may need to detect the screen reader and change the behaviour of that text when the reader is in use. The text may need to remain on the screen longer, or even wait until the user dismisses it. Adding the information necessary for the screen reader (which is called VoiceOver on iOS) to use an app is unbelievably easy. So easy in fact, that you should always plan to do it. It takes only a few moments per control and the benefit yield is incredibly high. Before we get to the code though, we need to become familiar with the screen reader itself. After all, you will need to test your app, and the testing plan needs to include the screen reader which means you need to know how to operate your iOS device the same way a blind person would. Before we enable the screen reader, you need to be aware that the devices interaction model changes when the reader is activated. Common gestures have been remapped to make the device more useable when the reader is active. That can be a bit disconcerting at first, but you will learn the new gestures as quickly and as easily as you learned the old ones. We will not cover all the gestures here as there are quite a few of them, but you do not need to be an expert in VoiceOver just yet. We will cover enough of the new gestures to help you navigate the system and to have a fairly solid understanding of the VoiceOver environment. The full list of gestures is covered in the iPhone User Guide under the chapter on Accessibility. The guide is available from the Apple website and is well worth reading.

Nicolaas tenBroek (AMDG)

339

iOS Development Gesture Single Tap Single Tap Number of Fingers 1 2

Accessibility

Action Taken Read the selected item Stop reading the selected item Activate the selected item (this is the single-fingered Double Tap 1 single tap item activation when VoiceOver is off) Activate the selected item (this is an alternative gesture Split Tap1 2 to the double-tap) Mute or Unmute the screen reader without actually Double Tap 3 turning off VoiceOver (gestures remain active) Triple Tap 3 Toggle Screen Curtain2 3 Flick Left 1 Select (and read) the next item Flick Right3 1 Select (and read) the previous item 3 Flick Left 3 Go to the previous page or screen Flick Right3 3 Go to the next page or screen Depends on Item selected and Rotor Control Settings. For example: Flick Up or Down3 1 If selected item is a UISlider, then flicks change the value. If reading text, flicks change between reading words or individual characters. Begin reading entire screen starting at the top of the Flick Up3 2 screen Flick Down3 2 Begin reading entire screen from the current location Scroll entire screen up or down (this is the regular Flick Up or Down3 3 single-fingered screen scroll when VoiceOver is off) 3 Flick Up 4 Select the first element on the screen Flick Down3 4 Select the last element on the screen Change selected Rotor option. The Rotors options are entirely dependent on its context. Thus the options Rotate 2 available will change as you select different items on the screen. 1. Split Taps are holding an item with one finger and tapping the screen with another finger. The fingers can be next to each other (i.e. from the same hand) or far apart (i.e. a two-handed operation). 2. The Screen Curtain is provided to help you test your app by turning off the screen display while keeping the screen active. You should always test your apps accessibility by using the curtain, as it will keep you from peeking. If you cannot use a portion of your app with the screen curtain active, then you know that the offending section will need to be redesigned. 3. Flicks are very fast and short gestures, not drags Before you attempt to add accessibility features into your app, you truly must understand how blind users interact with the system. As we indicated earlier, you do not have to be an expert in using VoiceOver, but you do need to be fairly competent with it. Without a decent level of proficiency, you simply will not be able to test your app in a realistic way, and may well end up producing an app that frustrates users rather than helping them. Thankfully, VoiceOver is quite well designed, and you will not need to invest much effort to become proficient. We recommend that you spend some time working with a couple of apps, and the built-in apps will work well for this practice. In our opinion, the

Nicolaas tenBroek (AMDG)

340

iOS Development

Accessibility

best app to begin practicing VoiceOver interactions is the calculator app. It is a fantastic training ground, as it has plenty of interactivity and it is quite safe, as you cannot accidentally destroy anything. When you have mastered the calculator, try accessing some e-mail. The e-mail app is also a good choice because it contains a great deal of text which will help you get used to listening to the computerised voice. You will likely need at most five minutes in each app to become proficient with the system. Be sure to enable the Screen Curtain during your practice! After all, you cannot hope to become proficient with VoiceOver if you peek at the screen each time you become a bit frustrated. This practice time is incredibly important, so do not skip it. The ten minutes it will take to make the investment will save you a great deal of effort and frustration later.

Nicolaas tenBroek (AMDG)

341

iOS Development

Accessibility

Accessibility Protocols
Accessibility is implemented in iOS apps through four separate protocols. The one you will most often work with is UIAccessibility and is responsible for providing access to accessibility information from the elements of a user interface. Each UI component you develop should implement UIAccessibility, and in fact all of the pre-built components that extend UIView already have. The second protocol is UIAccessibilityAction, which provides a way for UI components to support accessibility-type actions. For instance, the UISlider implements this protocol in order to support the flick up & down actions. You need only implement this protocol in custom UI components that a user interacts with as all the prebuilt UI components already provide the necessary support. The third protocol is UIAccessibilityFocus and is not directly involved with making components accessible to the user. Instead, it provides a programmatic interface that allows you to discover whether or not a specific component is currently the focus of an assistive technology. The fourth protocol is UIAccessibilityContainer. UIAccessibilityContainer provides a mechanism for indicating that a particular component is itself not accessible, but that its children are. This is useful in situations when a particular view (or component) contains other views and the user will not directly interact with the containing view, but will interact with the sub-views. UIAccessibility Protocol The last three protocols are fairly straightforward affairs. They contain a small number of well-defined methods (three each at the time of this writing) and are quite easy to understand. UIAccessibility on the other hand, adds no new methods, but does add a fair set of properties, which can be confusing. The purpose of the large number of properties is to divide up the accessibility information in such a way that dynamic changes to the information can be accomplished in a precise and directed fashion. The setup may seem a bit overly complicated at first, but the fine-grained nature of the divisions will be very useful when dealing with these items in code. We shall begin our investigation with an overview of the properties, and then discuss the potentially confusing items in more detail.

Nicolaas tenBroek (AMDG)

342

iOS Development Property Name isAccessibilityElement

Accessibility

Purpose A Boolean value that indicates whether or not a particular component can be accessed by an assistive technology. accessibilityLabel A label that concisely identifies the component. accessibilityHint A concise description of the result of interacting with the component. accessibilityValue The current value contained within or represented by the component. accessibilityTraits A set of traits that describe the element. accessibilityFrame The screen coordinates of the elements frame. accessibilityActivationPoint (iOS 5 and later) The centre of the accessibilityFrame accessibilityLanguage The language which should be used by VoiceOver to read the other properties.1 accessibilityElementsHidden (iOS 5 and later) A Boolean value that indicates whether or not VoiceOver should pay attention to this item. accessibilityViewIsModal (iOS 5 and later) A Boolean value that indicates this item is modal and siblings of this view should be ignored by VoiceOver. 1. While the localisation of an interface is both extremely important and supported by the assistive technologies, we will cover the topic in its own chapter. For now it will suffice for you to be aware that iOS fully supports localisation, and that includes the assistive technologies. When VoiceOver begins to describe a component to the user, it always follows the same pattern. First it reads the label, then it reads the value if it is appropriate, and then it describes the component from its combination of traits. If the user continues to select the item (i.e. holds a finger on the component), then VoiceOver will read the hint. On short presses the hint is not read so that users who are familiar with the app are not forced to listen to information they already know. It is incredibly important that you pay attention to this breakdown of information. For instance, you need to avoid repetition of information in the different properties, and use each property only for its intended purpose. Any deviation from this setup will cause users a great deal of frustration. In addition to the division of information, Apple has devised a set of guidelines for the way information is presented within each of the properties. These guidelines will help you structure your data in a way that makes it easier for VoiceOver to read to the user in as natural a voice as possible. Despite its mechanical sound, VoiceOver makes every attempt to read to the user with proper inflection based on the way words are provided to it. For instance, it can provide appropriate inflection when a word is followed by a period or a question mark, and can tell the difference between a capitalised word and one that begins with a lower-case letter. If you follow the guidelines as provided, VoiceOver will do a decent job at describing your app for a blind user. The guidelines for writing a label are fairly straightforward. A label should be as concise as possible while still being accurate. If one word accurately describes purpose the component, then by all means stop with that one word. If a phrase is necessary to be accurate, then provide the phrase with as few words as possible. Regardless of the number of words used in the label, the first one should begin with

Nicolaas tenBroek (AMDG)

343

iOS Development

Accessibility

a capital letter and no period should be used. Remember that something else will likely be read immediately after the label and a period will cause Voice Over to take an unnatural (and potentially confusing) pause. The label should never include information about the type of control. That information is provided by the traits, and must not be repeated. The hint is more complex than the label. It is read when the user actually wants more information from the system. Nonetheless, you should continue to be as concise as possible. Remember that the user actually wants to do something in your app, and that something is not listening to VoiceOver. Hints should be structured like sentences in that they should begin with an initial capital letter and end with a period. If at all possible the hint should begin with a pluralized verb. Avoid using verbs in the singular as they will sound like a command rather than a hint when read by VoiceOver. Remember that the hint is not telling the user to do something; instead it is telling them what the system will do when they interact with the component. The hint should not tell the user which gestures are necessary to activate the component as that information is provided elsewhere and may change based on system-wide settings. Finally, remember to avoid repeating information from the other properties (and no, the irony of repeatedly instructing you not to repeat things is not lost on us). The value property should be the actual value contained within the component. Most pre-built components set this property automatically as their value changes, so you will not typically need to interact with it directly. If you are designing your own component, then you must remember to set the property when the components value changes. The traits property is an interesting one. The traits are actually a set of integers and are combined with an OR operation into a single value. Again, the pre-built components already set their traits property, but you must remember that if you set traits within a custom component you are required to combine the components traits with that of its super class. Examples of the available traits are: Button, Link, Search Field, Image, and Updates Frequently. When setting this property you should reference all the available traits from the documentation so as to be sure you do not miss any.

Nicolaas tenBroek (AMDG)

344

iOS Development

Accessibility

Example App
By now you should have a sense of how all of these elements fit together. If you have not taken the time to practice using VoiceOver, then you should certainly do it now (with the screen curtain activated!). Accessibility decisions can potentially impact the design of an entire app, so rather than just demonstrate support with code snippets we decided to develop a mostly complete app for this example. We will be rewriting the shopping list app we developed in the Navigation chapter, but with some significant design changes. The first thing we will do is making the app simpler to use. Rather than forcing users to fill out a form in order to add an item to our list, we will provide a single text field, thereby allowing users to add whatever they want to the list. Frankly this is a much better approach even without considering accessibility implications. While this will probably come as a shock to those in either Human Resources or governmental posts, people really do not want to spend their days filling out forms. If you watch people actually create shopping lists on paper, they tend to write down lots of things that will not cleanly fit into a form. You may notice that people never start the process by drawing lots of rows and columns. App designers often seem to overlook issues like this, and even go too far in the wrong direction. In fact, if you search shopping list in the iTunes app store, you will find page after page of apps with ever more complicated forms for people to fill out. Most of those forms are so complicated because the designers have thought about all the kinds of information on a typical shopping list, and tried to create a field for each piece of data, covering every conceivable situation. Frankly, it just isnt worth the effort. Those apps quickly become too hard to use, and most seem to have lost sight of their original goal. The next time you are shopping, take a look at the lists people have. Many are written on scraps or re-used paper. Why is that? The lists are not intended to be permanent items, so as soon as they have purchased those items, the list is disposed of. It doesnt make sense to work for weeks to produce a product that stores information that will be used for a few minutes and then wind up in a bin. It makes even less sense when you spend all that time and effort to produce a product that is hard for those without disabilities to use, and is therefore next to impossible for those with disabilities. In addition to simplifying the interface for adding items, we will add support for multiple stores support for saving our lists. After all, a shopping list that cannot save its contents is not terribly useful. We will also attempt to add some advanced features without cluttering up the interface too much. Those features will include the ability to rearrange the items on the list, and support recurring items as well. After all, many people buy the same things at the same stores as part of their regular shopping habits. If we can save them from constantly re-entering the same items, then we will have done a good thing. And last, because our app will be incredibly simple, we can easily add support for basic saving and restoring the app state through the iOS multi-tasking model. Restoration of app state is a wonderful feature that makes our app more pleasant to use in general, but also makes it much easier for those with disabilities. The one thing we will not be doing is spending much time making the app pretty. We will leave that task as an activity for the reader. Before we look at the code, we do need to provide some of our own images (yes, more irony: images in an app designated to give you practice designing for the blind). We are going use the images as a means for the user to indicate when an item has been added to their shopping cart. While using a paper list Nicolaas tenBroek (AMDG) 345

iOS Development

Accessibility

people would scratch through the item once they had placed it in the cart, we will take a different approach. We could simply let the user delete the item from the list, but that presents two problems for us. The first problem is that we would lose the ability to make that a recurring item. The second problem is that if the item is deleted, the user can no longer see it. With a paper list, a person can always see the items they have scratched off, and thus verify that they have not forgotten something. We need to implement something similar, but rather than lining through the text, we will use both list organisation and these two simple images:

Here are a few screen shots of our app at work. First, we see a store called Wally World with a list of vague and unthreatening items to purchase:

The next screen shot shows a couple of items from within the list marked as purchased. Here you can see how our list will rearrange itself to keep the necessary (i.e. not yet purchased) items at the top, with a reminder of what we already have in the cart at the bottom:

Nicolaas tenBroek (AMDG)

346

iOS Development

Accessibility

This last screen shot shows what happens after our spouse calls in the middle of the shopping to add something to our list. The newly added item is placed at the bottom of the un-purchased section:

Our app will support multiple stores, each with its own shopping list, and the items in each list will maintain their state (purchased or not). So our data model will involve three classes, though each of them will be straightforward. To support saving the lists to files we decided to implement the NSCoding Nicolaas tenBroek (AMDG) 347

iOS Development

Accessibility

protocol in our data classes. This is by no means the only approach to the problem, nor is it necessarily the best approach. NSCoding works well for serialising and de-serialising objects, but does not work at all for enabling access to the data in other formats. For instance, if we decided that access to the lists from a computer was important (i.e. so you could edit it directly from your desktop when the phone was plugged in), then we would need to save the data to text files instead. We might also have some reason for using a database, perhaps one in the cloud rather than local storage. Our decision for this project was based entirely on expediency. The NSCoding protocol provided the fastest means to complete the example app with the ability to save lists. We created this example by starting with the empty app template, and named the project a rather creative ShoppingList. Our first class is the bottom of the data model chain. This class represents a single item on a list. An item consists of only two parts: a string for content and a Boolean state indicator. Instances of ListItem will need to be able to notify their container when they are modified, so we also create a protocol called ItemWatcher. As you can see the ItemWatcher protocol contains only one method called itemUpdated:. ListItem.h
#import <Foundation/Foundation.h> @protocol ItemWatcher; @interface ListItem : NSObject <NSCoding> @property (nonatomic, retain) NSString *itemDescription; @property (nonatomic, assign) BOOL isPurchased; @property (nonatomic, strong) id<ItemWatcher> delegate; @end @protocol ItemWatcher <NSObject> - (void)itemUpdated:(ListItem *)item; @end

The ListItem implementation starts with the declaration of instance variables and some constants. The NSCoder supports a keyed-method for storing data (among others), and we opted for that approach here. The process used by NSCoder is very similar to that of a dictionary which means each piece of data needs its own unique key, so care must be used in choosing the key. Our app is quite simple, so creating unique keys is not difficult and we decided it was safe enough to disperse the keys in the source files. A larger project would require centralised keys to ensure the uniqueness of the strings, and the App Delegate would be the best place to handle that data (see the Preferences and Settings chapter for an example). We used constants to ensure the keys are not accidentally changed, and made them static to reduce memory consumption.

Nicolaas tenBroek (AMDG)

348

iOS Development

Accessibility

The NSCoding protocol defines two methods: initWithCoder: and encodeWithCoder:. The method encodeWithCoder: is called during the save operation, and the initWithCoder: method is called during the read operation. ListItem.m (section 1 of 2)
#import "ListItem.h" @implementation ListItem { @private NSString *itemDescription; BOOL isPurchased; id<ItemWatcher> delegate; } static NSString *const DESCIPTION_KEY = @"ITEM_DESCRIPTION"; static NSString *const PURCHASED_KEY = @"ITEM_PURCHASED"; @synthesize itemDescription; @synthesize isPurchased; @synthesize delegate; #pragma mark - NSCoding Protocol - (id)initWithCoder:(NSCoder *)decoder { if((self = [super init])) { self.itemDescription = [decoder decodeObjectForKey:DESCIPTION_KEY]; self.isPurchased = [decoder decodeBoolForKey:PURCHASED_KEY]; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:self.itemDescription forKey:DESCIPTION_KEY]; [encoder encodeBool:self.isPurchased forKey:PURCHASED_KEY]; }

Next we need to override our setter methods. This is necessary so that we have the opportunity to notify our observer of changes to instances of ListItem. With the custom setters in place this data class is complete. ListItem.m (section 2 of 2)
#pragma mark - Custom Setters - (void)setItemDescription:(NSString *)newItemDescription { itemDescription = newItemDescription; [delegate itemUpdated:self];

Nicolaas tenBroek (AMDG)

349

iOS Development ListItem.m (section 2 of 2)


} - (void)setIsPurchased:(BOOL)newIsPurchased { isPurchased = newIsPurchased; } @end [delegate itemUpdated:self];

Accessibility

The next step in our data model chain is the store. The store is only slightly more complicated than the item class. Here we again implement the NSCoding protocol, and add to that the ItemWatcher protocol. We also define a similar protocol called StoreWatcher. Although the store will be responsible for saving itself to a file, we will need to notify the store collection class in cases where the name of the store has been updated so that the store collection can update its display. Like the list item, the stores data is also quite simple, containing only a name, an array of items, a file name, and delegate. The store provides a number of data accessor methods. Remember that the principles of Implementation Hiding tell us to prevent direct access to our data storage mechanism which then allows us to change the class later if it turns out that an array no longer suits our purpose. If we have done a decent job at hiding the implementation, then the change of data storage mechanism will not affect any other classes. In addition to the standard add, remove, and get type methods; we also provide a method for moving the location of an item. This is to support user-defined list arrangement within the shopping list itself. Finally we add a save method which will be used to initiate saving the store to a file. Store.h
#import <Foundation/Foundation.h> #import "ListItem.h" @protocol StoreWatcher; @interface Store : NSObject <NSCoding, ItemWatcher> @property (nonatomic, readonly) NSString *fileName; @property (nonatomic, strong) NSString *name; @property (nonatomic, retain) id<StoreWatcher> delegate; - (id)initWithName:(NSString *)storeName; (void)addItem:(ListItem *)item; (void)removeItemAtIndex:(int)index; (void)moveItemAtIndex:(int)fromIndex toIndex:(int)toIndex; (int)numItems; (ListItem *)itemAtIndex:(int)index;

Nicolaas tenBroek (AMDG)

350

iOS Development Store.h


- (void)save; @end @protocol StoreWatcher <NSObject> - (void)storeUpdated:(Store *)store; @end

Accessibility

We begin our Stores implementation in the same way as we did the Items, by creating keys for storing the pieces of data. You will note that we overrode the default init method and used it to call the new initWithName: method. In order for the Store to save itself to a file it needs to ensure the file name is unique. We will be basing the file name partially on a Stores name, so the default init method could not be used. Overloading it in this fashion ensures we always have a Store name, even if we accidentally call the wrong method somewhere. In initWithName: we create a unique file name by pairing the store name with the timestamp obtained when the Store object was created. That ensures we will get unique names even if a user creates multiple Store objects with the same name. In the NSCoding protocol methods we write all the data to the file, including the file name. While it may seem odd to store the name of the file in the file itself, it is not as strange as it first appears for two reasons. First, the initWithCoder: method does not know the name of the name of the file being read from, so the only way to re-write the updated date to the same file is to store the file name in the file. Second, in apps which serialise the app data as a means of saving the app state, the same initWithCoder: and encodeWithCoder: methods would be used. In our case that means we will use the same methods when saving the shopping list and when saving the app state. Given that we cannot tell where the object is being saved, we are forced store all of the objects information. With that single exception, these methods are essentially identical to the ones we used in the ListItem class. Notice that to encode and decode the list items we need only encode and decode the array object. The arrays encode and init methods will call encode and init for each item within the array. Store.m (section 1 of 3)
#import "Store.h" @implementation Store { @private NSString *fileName; NSMutableArray *items; NSString *name; id<StoreWatcher> delegate; } static NSString *const FILE_NAME_KEY = @"FILE_NAME"; static NSString *const NAME_KEY = @"STORE_NAME";

Nicolaas tenBroek (AMDG)

351

iOS Development Store.m (section 1 of 3)


static NSString *const ITEMS_KEY = @"STORE_ITEMS"; @synthesize fileName; @synthesize name; @synthesize delegate; - (id)init { return [self initWithName:@"Unknown Store"]; }

Accessibility

- (id)initWithName:(NSString *)storeName { if((self = [super init])) { self.name = storeName; fileName = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%lf.store", storeName, [[NSDate date] timeIntervalSince1970]]]; items = [[NSMutableArray alloc] init]; } } return self;

#pragma mark - NSCoding Protocol - (id)initWithCoder:(NSCoder *)decoder { if((self = [super init])) { fileName = [decoder decodeObjectForKey:FILE_NAME_KEY]; name = [decoder decodeObjectForKey:NAME_KEY]; items = [decoder decodeObjectForKey:ITEMS_KEY]; } } return self;

- (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:fileName forKey:FILE_NAME_KEY]; [encoder encodeObject:name forKey:NAME_KEY]; [encoder encodeObject:items forKey:ITEMS_KEY]; }

In the next section we see the data accessor methods. Each method that changes the shopping list also calls the save method. This may be a bit of overkill, but the lists themselves will be so short that the user will not notice the saving. Store.m (section 2 of 3)
#pragma mark - Item Management - (void)addItem:(ListItem *)item {

Nicolaas tenBroek (AMDG)

352

iOS Development Store.m (section 2 of 3)


if(item) { [items addObject:item]; item.delegate = self; [self save]; } } - (void)removeItemAtIndex:(int)index { if((index >= 0) && (index < [items count])) { [items removeObjectAtIndex:index]; [self save]; } }

Accessibility

- (void)moveItemAtIndex:(int)fromIndex toIndex:(int)toIndex { if((fromIndex >= 0) && (fromIndex < [items count]) && (toIndex >= 0) && (toIndex <= [items count])) { if(toIndex > fromIndex) { toIndex++; } [items insertObject:[items objectAtIndex:fromIndex] atIndex:toIndex]; if(fromIndex > toIndex) { fromIndex++; } [items removeObjectAtIndex:fromIndex]; [self save]; } } - (int)numItems { return [items count]; } - (ListItem *)itemAtIndex:(int)index { if((index >= 0) && (index < [items count])) { return [items objectAtIndex:index]; } return nil; }

In this final section we start with a custom setter method. We are manually implementing this setter for the same reason as we had in the ListItem class. Whenever the name of the store is changed we will update our observer. The observer does not need to know when the contents of the list have changed, so we only notify about changes that will affect the display of the store.

Nicolaas tenBroek (AMDG)

353

iOS Development

Accessibility

We also implement our save method in this section. As you can see, the save is quite simple. We supply the file name to an archiver (which is an instance of NSCoder) and the object to be saved. This will open the file and then call the encode method. Finally, we implement the ItemWatcher protocol. Just as we did with the data handling methods, if any list item changes we save the entire list. Store.m (section 3 of 3)
#pragma mark - Custom Setters - (void)setName:(NSString *)newName { name = newName; [self save]; [delegate storeUpdated:self]; } #pragma mark - File Handling - (void)save { [NSKeyedArchiver archiveRootObject:self toFile:fileName]; } #pragma mark - ItemWatcher Protocol - (void)itemUpdated:(ListItem *)item { [self save]; } @end

The next stage in our data model is the store collection. This class is responsible for maintaining the list of stores. Again we will demonstrate proper OOP techniques by hiding the implementation and providing accessor methods. This design will be a bit simpler than the stores as the user will not be allowed to modify the order of the stores in the list. Despite its simple design, the store collection class will have a unique feature within our data model. We need access to the collection from several different places within our application, so we decided to implement the class using a singleton pattern. With that in mind we included in our header file a class method called instance. The instance method can then be used anywhere within the app to get the one collection of stores. The inability to control access to the methods that create instances (i.e. constructors) means creating singleton classes in Objective-C is a bit tricky and the creation of the singleton instances must be handled quite differently from the way they are in other languages. Because of this complication many approaches have been developed to address the problem. The approach we will present is the simplest to implement, and therefore (we believe) the best as it will be the easiest to implement properly.

Nicolaas tenBroek (AMDG)

354

iOS Development

Accessibility

The store collection does not have any unique or identifying information in it, so we did not really need to implement the NSCoding protocol as we have the other classes. Instead we will handle its creation and the restoration of data through the singleton. StoreCollection.h
#import <Foundation/Foundation.h> #import "Store.h" @interface StoreCollection : NSObject <StoreWatcher> + (StoreCollection *)instance; (int)numberOfStores; (void)addStore:(Store *)store; (Store *)storeAtIndex:(int)index; (void)removeStoreAtIndex:(int)index;

- (void)save; @end

We start our store collection implementation with a private category. The stores will be displayed in a sorted order, which necessitates a sort method. There really is no need for an outside class to have access to the sort, so we have made it private. Next we declare the array of stores and a static StoreCollection instance variable (which we rather creatively named instance). This will be used to hold our singleton. Creation of the singleton is handled in the class method initialize. The initialize method is called automatically the very first time a class is used in each run of the app. Its implementation can be a bit of a trick though because it would seem from its description that initialize could only be called once. The truth of the matter is that initialize is called once for each class in an inheritance tree. For example, imagine we had three classes: A, B, and C in which A was the root, B extended A, and C extended B. If we create an instance of A, As initialize method would be called once. If we then created an instance of B, As initialize would be called again, and then Bs would be called. If we then created an instance of C, we would see As and then Bs and then Cs initialize methods being called (note: that is a total of three calls for A and two for B). This situation is further complicated by inheritance. If A and B both provided an implementation of initialize, but C did not, then the call to Cs initialize would be passed to B, resulting in Bs initialize being called two times in a row. Even worse, some misguided soul might directly call initialize, which can further complicate the situation. As the author of an initialize method, it is your responsibility to ensure you do not accidentally over-create objects or cause memory leaks. There are two checks you need to make in order to ensure initialize is run properly. First, you can check to see if the current call was due to this class being initialised, or if it was a result of a subclass being initialised. We do this by accessing the self-variable. In a class method the self-variable is set to an instance of class Class that represents the class used in the current object

Nicolaas tenBroek (AMDG)

355

iOS Development

Accessibility

creation step. So, if self does not equal an instance of the current class, then you know a subclass is being initialised. The second check is simple enough, you need only check to see whether or not your instance data has already been created. If the data has been created, then you are free to ignore the current call. StoreCollection.m (section 1 of 4)
#import "StoreCollection.h" @interface StoreCollection (StoreCollectionPrivate) - (void)sort; @end @implementation StoreCollection { @private NSMutableArray *stores; } static StoreCollection *instance; + (void)initialize { if((self == [StoreCollection class]) && (!instance)) { instance = [[StoreCollection alloc] init]; } } + (StoreCollection *)instance { return instance; }

The next section of StoreCollection covers the init method. This is by far the largest method in the class and handles a critical task. This method scans the Documents directory and attempts to load a store for each file found. We could have been a bit more selective in our approach to identifying files here as we did use an extension of .store on our file names (in the Store class), but at the moment stores are the only things our app is saving in this directory. If we later add other data to this folder, then making this search more specific would be a good idea. Once all the store files have been loaded we sort our array. Note that at the very beginning of the init method we check to see whether or not instance exists. If instance does exist then we know someone not using the singleton has called this method. In those cases we will skip the initialisation and return nothing. StoreCollection.m (section 2 of 4)
- (id)init { if((!instance) && (self = [super init])) { stores = [[NSMutableArray alloc] init]; NSFileManager *fileManager = [[NSFileManager alloc] init];

Nicolaas tenBroek (AMDG)

356

iOS Development StoreCollection.m (section 2 of 4)

Accessibility

NSError *error = nil; NSString *documentsDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; NSArray *fileNames = [fileManager contentsOfDirectoryAtPath:documentsDirectory error:&error]; if(error) { NSLog(@"Error reading Documents Directory: %@", error); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error Locating Stores" message:@"Unable to locate saved stores." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; } for(NSString *fileName in fileNames) { Store *aStore = [NSKeyedUnarchiver unarchiveObjectWithFile: [documentsDirectory stringByAppendingPathComponent:fileName]]; if(aStore) { [stores addObject:aStore]; } else { NSLog(@"Error Processing File: %@", fileName); } } } } [self sort];

return self;

Our accessor methods are fairly basic, with the exception of removeStoreAtIndex:. If a store is to be deleted we must remember to delete the associated file, or the deleted store will be resurrected the next time the app is launched. Zombie stores might make a good game, but would make a poor shopping list. Note that if the file fails to delete (which admittedly would be odd); we must inform the user and not remove the store from the list in order to prevent confusion. StoreCollection.m (section 3 of 4)
#pragma mark - Store Management - (int)numberOfStores { return [stores count]; } - (void)addStore:(Store *)store { [stores addObject:store]; store.delegate = self; [self sort]; }

Nicolaas tenBroek (AMDG)

357

iOS Development StoreCollection.m (section 3 of 4)


- (Store *)storeAtIndex:(int)index { if((index >= 0) && (index <= [stores count])) { return [stores objectAtIndex:index]; } return nil; }

Accessibility

- (void)removeStoreAtIndex:(int)index { if((index >= 0) && (index <= [stores count])) { NSFileManager *fileManager = [[NSFileManager alloc] init]; NSError *error = nil; [fileManager removeItemAtPath:[[stores objectAtIndex:index] fileName] error:&error]; if(error) { NSLog(@"Error deleting file %@", error); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error Deleting Store" message:@"Unable to delete store" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; } else { [stores removeObjectAtIndex:index]; } } }

In the final section of code for this class we implement the StoreWatcher protocol. The only likely event causing this notification is a change to the stores name, so we can use this notification to re-sort the array. We have also implemented a save method here which provides us a means of quickly saving all the stores. This really should not be necessary because the stores save themselves every time an item is updated, but this method will ensure that all situations are covered (i.e. any we have not yet thought of). Next we implement our private sort method. The NSMutableArray class provides a method called sortUsingComparator: which takes a code block as its argument. The array passes the code block all of the elements (two at a time) and expects a return value of type NSComparisonResult. The array then uses that return value to rearrange the elements. Here we are simply sorting on the store names, though we used a case-insensitive sort option. StoreCollection.m (section 4 of 4)
#pragma mark - StoreWatcher Protocol - (void)storeUpdated:(Store *)store {

Nicolaas tenBroek (AMDG)

358

iOS Development StoreCollection.m (section 4 of 4)


} [self sort];

Accessibility

#pragma mark - File Handling - (void)save { for(Store *store in stores) { [store save]; } } #pragma mark - Private Methods - (void)sort { [stores sortUsingComparator: ^(id store1, id store2) { return [[store1 name] compare:[store2 name] options:NSCaseInsensitiveSearch]; }]; } @end

Those three classes are all we need for our data model, but before we move into the user interfaces, we should update the app delegate to provide the necessary steps for pausing and restoring the app state. Additionally we will need to setup the navigation for the app, which is a process we have seen several times already and therefore should be familiar to you. AppDelegate.h
#import <UIKit/UIKit.h> @class RootViewController; @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) RootViewController *rootViewController; @property (nonatomic, strong) UINavigationController *navigationController; @end

Nicolaas tenBroek (AMDG)

359

iOS Development

Accessibility

To support the saving of data we need to implement the methods applicationWillResignActive: and applicationWillTerminiate:. Those two methods simply ensure that the stores and the app state are stored. Remember that the method applicationWillTerminate: is only called on older versions of iOS that do not support multitasking and does not need to be implemented if your app cannot support the older OS versions. AppDelegate.m
#import "AppDelegate.h" #import "RootViewController.h" #import "StoreCollection.h" @implementation AppDelegate @synthesize window; @synthesize rootViewController; @synthesize navigationController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.rootViewController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil]; self.navigationController = [[UINavigationController alloc] initWithRootViewController:self.rootViewController]; [self.window addSubview:navigationController.view]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { [[StoreCollection instance] save]; } - (void)applicationWillTerminate:(UIApplication *)application { [[StoreCollection instance] save]; } @end

For our next class we need a new subclass of UIView. Technically, we could have done without this view and created an editable cell in our table displaying the stores. After all, the only piece of information we need in order to create a store is the stores name. Admittedly, devoting an entire screen to a single text field seems like overkill, but there is reason behind the apparent madness. When designing apps with accessibility in mind, you will find such design changes make life a great deal easier for those who cannot see the screen. If we used the editable cell rather than a second screen, a sighted person would see the changes in the table, and quickly piece together what was happening. Unfortunately for the

Nicolaas tenBroek (AMDG)

360

iOS Development

Accessibility

blind users, VoiceOver does not have the ability to read such subtle changes in appearance and context. So, if we did use in-cell editing, VoiceOver would continue treat the table as a table, reading each row, and giving a rather crazy impression of the goings on. The collection of stores will use our store creation view to both create and edit the store names, so we will start by defining a protocol that allows us to inform the table of changes. We also need properties for the text field, the delegate, and the store (used in the edit cases). CreateStoreViewController.h
#import <UIKit/UIKit.h> #import "Store.h" @protocol CreateStoreDelegate; @interface CreateStoreViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) IBOutlet UITextField *nameField; @property (nonatomic, weak) id<CreateStoreDelegate> delegate; @property (nonatomic, weak) Store *store; @end @protocol CreateStoreDelegate <NSObject> - (void)storeCreated:(Store *)store; - (void)storeUpdated:(Store *)store; @end

The screen for CreateStoreViewController will use a save button on the navigation bar, but we want to prevent saving of an empty field. After all, an empty store field would look pretty strange in our table and may well confuse both the blind and the sighted. To help prevent an accidental save we will implement the UITextFieldDelegate protocol, and use its notifications to enable and disable the save button as appropriate. We will also capture the return button press from the keyboard, and use that as a secondary save button. CreateStoreViewController.m (section 1 of 3)
#import "CreateStoreViewController.h" @interface CreateStoreViewController(CreateStoreViewControllerPrivate) - (void)save; @end @implementation CreateStoreViewController @synthesize nameField; @synthesize delegate;

Nicolaas tenBroek (AMDG)

361

iOS Development CreateStoreViewController.m (section 1 of 3)


@synthesize store; #pragma mark - UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { if([textField.text length]) { [self save]; } return YES; } - (void)textFieldDidBeginEditing:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = [textField.text length] > 0; } - (void)textFieldDidEndEditing:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = [textField.text length] > 0; } - (BOOL)textFieldShouldClear:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = NO; return YES; }

Accessibility

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { int length = [textField.text length] - range.length + [string length]; self.navigationItem.rightBarButtonItem.enabled = (length > 0); return YES; }

Next we have the save method. We will need to handle save differently depending on whether we are editing or creating a store. So, the first thing we do is examine the store property to see if it is nil. A nil property means we need to create a new store; non-nil means we need only update the name. When the appropriate changes are complete we inform the delegate and then return to the previous view. CreateStoreViewController.m (section 2 of 3)
#pragma mark - Actions - (void)save { if(!store) { Store *newStore = [[Store alloc] initWithName:self.nameField.text]; [delegate storeCreated:newStore]; } else { store.name = nameField.text; [delegate storeUpdated:store]; }

Nicolaas tenBroek (AMDG)

362

iOS Development CreateStoreViewController.m (section 2 of 3)


} [self.navigationController popViewControllerAnimated:YES];

Accessibility

In the next section we handle the screen setup so that the title accurately reflects the impending action (store creation or editing the name), but we also change the accessibility hint of the store field. Remember that the hint is only read if a person executes a long press, which means they need more information about the situation. Changing the information dynamically to match the situation is super easy, and should always be considered. Notice we also begin with the save button enabled for existing stores, and disabled for new stores. After the view is set up we ask the text field to become the First Responder. This call will cause the keyboard to appear immediately after the screen displayed, saving the user an extra tap. We have mentioned this several times before, but it is worth additional attention. Mobile apps are rarely used in ideal situations (though they are nearly always designed and tested in them), so any steps you can save the user will be greatly appreciated. We also need to ask the UIAccessibility system to post a notification about the screen change. This posting will cause VoiceOver to read the notification aloud, informing the user of the change in context. Remember to keep these notifications as concise and informative as possible. In viewDidAppear: you will notice we post an appropriate notification based on whether we are editing or creating a new store. CreateStoreViewController.m (section 3 of 3)
#pragma mark - Standard View Controller Methods - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save)]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:(BOOL)animated]; if(!store) { self.title = @"Create New Store"; self.nameField.accessibilityHint = @"Accepts new store name."; } else { nameField.text = store.name; self.title = @"Edit Store"; self.nameField.accessibilityHint = @"Edits existing store name."; } [nameField becomeFirstResponder]; }

Nicolaas tenBroek (AMDG)

363

iOS Development CreateStoreViewController.m (section 3 of 3)

Accessibility

- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if(!store) { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"Creating new store."); } else { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [NSString stringWithFormat:@"Editing store %@.", store.name]); } } @end

With the controller complete we can turn our attention to the view itself. In Interface Builder, setup the screen with a label and a text field, just as you normally would. Once you are satisfied with the setup of your screen, select the text field and open the Identity Inspector. Near the middle of the inspector screen you will note a section titled Accessibility. This section allows us to setup most of the accessibility information at design time, and should certainly be used for any information that is not going to change at run time. In the screen shot below we have included the label, but not the hint. Be sure to follow the guidelines when entering text in these fields. The label should be short (one word if possible) and must not end with a period. We did not include a hint here because we dynamically set the hint in the code previously.

Nicolaas tenBroek (AMDG)

364

iOS Development

Accessibility

With the store creation view completed we can now focus on building the table to display the stores. This class will actually be a very straightforward subclass of UITableViewController, needing nothing more than the standard table-type operations. We need to display the list of stores, allow for new stores to be created, and handle existing stores being edited. We will not need to worry about the data model, because we have already constructed it. We will use the StoreCollection singleton as a data model, so the only change we need to make in RootViewControllers header file is to indicate that we are implementing the CreateStoreDelegate protocol. RootViewController.h
#import <UIKit/UIKit.h> #import "CreateStoreViewController.h" @interface RootViewController : UITableViewController <CreateStoreDelegate> @end

We will start the tables implementation by taking care of the CreateStoreDelegate. The methods for that protocol are extremely simple, as they mostly just inform the table that the data has changed and therefore the display needs to be refreshed. After the delegate we have the action method that will be called when the user presses the add store button on the navigation bar. The code in this method is nearly boilerplate for a navigation-based app, so you should not see anything new in it. RootViewController.m (section 1 of 4)
#import "RootViewController.h" #import "ShoppingListViewController.h" #import "StoreCollection.h" @implementation RootViewController #pragma mark - CreateStoreDelegate - (void)storeCreated:(Store *)store { [[StoreCollection instance] addStore:store]; [self.tableView reloadData]; } - (void)storeUpdated:(Store *)store { [self.tableView reloadData]; } #pragma mark - Actions - (void)addStore { CreateStoreViewController *csvc = [[CreateStoreViewController alloc] initWithNibName:@"CreateStoreViewController" bundle:nil];

Nicolaas tenBroek (AMDG)

365

iOS Development RootViewController.m (section 1 of 4)


csvc.delegate = self; [self.navigationController pushViewController:csvc animated:YES]; }

Accessibility

In the method viewDidLoad we add two buttons to the navigation bar: one to add a store, and another to edit the table. Remember that as this view is the root of the tree the left side of the navigation bar is empty, which means we can safely add the edit button there. The only editing we are going to allow is to delete a store, so not much has to be done in support of that feature. In the next code section you will notice that we set the separator colour to a light brown. In an attempt to make this app a little less stock in appearance we set the tables colour to a yellowish colour. Making the separator brown helps it to stand out a bit more than the default clear setting. We also applied the yellow colour to all the other screens so that they all look alike. (Oy! Vey ist mir! Yet more irony! We have worried about the appearance of an app designed for demonstrating accessibility for the blind!) In the method viewDidAppear: we take an extra step to see whether or not the list of stores is empty. If the list is empty then literally nothing can be done in the app. So, when that is the case we call the addStore method straight away to take the user to the store creation screen. We will institute a similar behaviour in the shopping list later, albeit with a minor difference. RootViewController.m (section 2 of 4)
#pragma mark - Standard View Controller Methods - (void)viewDidLoad { [super viewDidLoad]; self.title = @"Stores"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addStore)]; self.navigationItem.leftBarButtonItem = self.editButtonItem; self.tableView.separatorColor = [UIColor colorWithRed:0.729 green:0.639 blue:0.47 alpha:1.0]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if(![[StoreCollection instance] numberOfStores]) { [self addStore]; } }

The UITableViewDataSource methods are nearly boilerplate as well. In fact, we only have two unique actions in tableView:cellForRowAtIndexPath:. We are using a standard built-in cell format, but each cell needs a disclosure button. As the button will be on every single cell we can add it when the cell is

Nicolaas tenBroek (AMDG)

366

iOS Development

Accessibility

created and will not need to re-add it each time the cell is repopulated. The other item that differs from the tables we have created to this point is that we set the accessibility hint. The hint includes the name of the store, so it needs to be reset each time a cell is populated. In tableView:commitEditingStyle:forRowAtIndexPath: we provide support for deleting a store. We do that simply by asking the data model to delete the appropriate store and then ask the table to remove the relevant cell with a fade animation. This method too is mostly standard and matches the actions we have used before with tables. The one deviation from our previous implementations is that we post an accessibility notification to let the blind users know that the deletion of the store is happening. To ensure our communication is as clear as possible we include the name of the store in the announcement. The next method is tableView:didSelectRowAtIndexPath:. If the user selects a cell then we will load the shopping list for the selected store and display it. This is a completely standard method and matches exactly what we have done in the past. RootViewController.m (section 3 of 4)
#pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[StoreCollection instance] numberOfStores]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; } cell.textLabel.text = [[[StoreCollection instance] storeAtIndex:indexPath.row] name]; cell.accessibilityHint = [NSString stringWithFormat:@"Selects shopping list for %@.", cell.textLabel.text]; return cell;

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; }

Nicolaas tenBroek (AMDG)

367

iOS Development RootViewController.m (section 3 of 4)

Accessibility

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [NSString stringWithFormat:@"Deleting store %@.", [[StoreCollection instance] storeAtIndex:indexPath.row].name]); [[StoreCollection instance] removeStoreAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { ShoppingListViewController *slvc = [[ShoppingListViewController alloc] initWithNibName:@"ShoppingListViewController" bundle:nil]; slvc.store = [[StoreCollection instance] storeAtIndex:indexPath.row]; [self.navigationController pushViewController:slvc animated:YES]; }

In the UITableViewDelegate section we implement the method tableView:accessoryButtonTappedForRowWithIndexPath:. The disclosure buttons automatically calls this rather unusual method, which is why those buttons did not have selectors assigned to them when they were added to the cells. In response to the button press we setup the store for editing, which you will note is a process very similar to what we used for creating a store. With this method written our store creation and management is complete. RootViewController.m (section 4 of 4)
#pragma mark - Table View Delegate - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { CreateStoreViewController *csvc = [[CreateStoreViewController alloc] initWithNibName:@"CreateStoreViewController" bundle:nil]; csvc.store = [[StoreCollection instance] storeAtIndex:indexPath.row]; csvc.delegate = self; [self.navigationController pushViewController:csvc animated:YES]; } @end

Next we need to work on the shopping list. We will use the same design pattern that we used for the stores: one view for creating the list items and a second using a table for displaying the list. The view for creating items will also allow editing, and will need only a single text field, so it will be nearly identical to what we created for the stores. Just as we did in the store creation view, we will need to define a

Nicolaas tenBroek (AMDG)

368

iOS Development

Accessibility

protocol that we can use to inform a delegate when an item has been created or updated. Our view will need three properties: a list item for use while editing, a delegate, and an outlet to the text field. We will also implement the UITextFieldDelegate in the same way as we did in the CreateStoreViewController. In fact, the only difference between ListItemViewController and CreateStoreViewController is the item being edited. If this situation doesnt scream out for refactoring, then nothing does. We will leave such refactoring as an exercise for the reader. ListItemViewController.h
#import <UIKit/UIKit.h> #import "ListItem.h" @protocol ListItemDelegate; @interface ListItemViewController : UIViewController <UITextFieldDelegate> @property (nonatomic, weak) ListItem *item; @property (nonatomic, weak) id<ListItemDelegate> delegate; @property (nonatomic, weak) IBOutlet UITextField *descriptionTextField; @end @protocol ListItemDelegate <NSObject> - (void)listItemCreated:(ListItem *)newItem; - (void)listItemUpdated:(ListItem *)updatedItem; @end

ListItemViewController.m
#import "ListItemViewController.h" @implementation ListItemViewController @synthesize item; @synthesize delegate; @synthesize descriptionTextField; #pragma mark - Private Methods - (void)saveItem { if(item) { item.itemDescription = descriptionTextField.text; [delegate listItemUpdated:item]; } else { ListItem *newItem = [[ListItem alloc] init]; newItem.itemDescription = descriptionTextField.text; [delegate listItemCreated:newItem];

Nicolaas tenBroek (AMDG)

369

iOS Development ListItemViewController.m


} } [self.navigationController popViewControllerAnimated:YES];

Accessibility

#pragma mark - UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { if([textField.text length]) { [self saveItem]; } return YES; } - (void)textFieldDidBeginEditing:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = [textField.text length] > 0; } - (void)textFieldDidEndEditing:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = [textField.text length] > 0; } - (BOOL)textFieldShouldClear:(UITextField *)textField { self.navigationItem.rightBarButtonItem.enabled = NO; return YES; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { int length = [textField.text length] - range.length + [string length]; self.navigationItem.rightBarButtonItem.enabled = (length > 0); return YES; } #pragma mark - Setters - (void)setItem:(ListItem *)listItem { item = listItem; if(item) { self.descriptionTextField.text = item.itemDescription; }

#pragma mark - UIViewController - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveItem)];

Nicolaas tenBroek (AMDG)

370

iOS Development ListItemViewController.m


self.title = (item)? @"Edit Item" : @"Create Item"; self.descriptionTextField.text = item.itemDescription; [self.descriptionTextField becomeFirstResponder];

Accessibility

} @end

Before we move on to the table that will handle the shopping list, we need to create a custom cell. In addition to a customised display, this cell will need to be able to change the purchased state of a ListItem. You will recall from the chapter on tables that the best way to add behaviours is to subclass UITableViewCell, and thankfully this will be an easy task. Our cell will consist of a label to display the shopping list item, and a button to toggle the purchased state. The button will be displaying either the checked or unchecked image only, and will call the togglePurchased action method. ListItemCell.h
#import <UIKit/UIKit.h> @interface ListItemCell : UITableViewCell extern NSString *const LIST_ITEM_CELL_IDENTIFIER; @property (nonatomic, weak) IBOutlet UILabel *itemLabel; @property (nonatomic, weak) IBOutlet UIButton *purchasedButton; @property (nonatomic, assign) BOOL isPurchased; - (IBAction)togglePurchased; @end

The implementation of our cell is quite simple. We need only to load the images and implement the necessary code to handle the items state. As the state could be set by either pressing the button or from an external source, we decided to place all the state handling code in the setter method. Our setter method changes not only the image displayed on the button, but also modifies the accessibility information. The accessibility label for the button simply toggles between purchased and not purchased depending on its state, but the UILabels accessibility label needs a bit more. VoiceOver will need to be able to read the label to the user, so we put the entire text in the accessibility label. If the item is purchased we preceded the text with the phrase Purchased item and used a period to ensure that VoiceOver pauses before reading the item description.

Nicolaas tenBroek (AMDG)

371

iOS Development ListItemCell.m


#import "ListItemCell.h" @implementation ListItemCell { @private UIImage *checkedImage; UIImage *uncheckedImage; BOOL isPurchased; } NSString *const LIST_ITEM_CELL_IDENTIFIER = @"ListItemCell"; @synthesize itemLabel; @synthesize purchasedButton; @synthesize isPurchased; - (id)initWithCoder:(NSCoder *)decoder { if ((self = [super initWithCoder:decoder])) { checkedImage = [UIImage imageNamed:@"CheckMark.png"]; uncheckedImage = [UIImage imageNamed:@"CheckBox.png"]; } return self; }

Accessibility

- (void)setIsPurchased:(BOOL)isPurchasedNow { isPurchased = isPurchasedNow; if(isPurchased) { [purchasedButton setImage:checkedImage forState:UIControlStateNormal]; self.purchasedButton.accessibilityLabel = @"Purchased"; self.itemLabel.accessibilityLabel = [NSString stringWithFormat:@"Purchased item. %@", self.itemLabel.text]; } else { [purchasedButton setImage:uncheckedImage forState:UIControlStateNormal]; self.purchasedButton.accessibilityLabel = @"Not Purchased"; self.itemLabel.accessibilityLabel = self.itemLabel.text; } } - (IBAction)togglePurchased { self.isPurchased = !self.isPurchased; } @end

The last class we need to write is the table to handle the shopping list. This class will be the most complex of the entire app because its view is where users will be spending the majority of their time, and it will need the most behaviour. The shopping list as maintained in our data model does not differentiate between items that are marked as purchased, and those that are not. All of the items are simply stored in one big array. We made the

Nicolaas tenBroek (AMDG)

372

iOS Development

Accessibility

decision to store the items in this way because it greatly simplifies the ability to make items recurring and handle user arrangement of the items. If we let the user rearrange the list, but then lost that arrangement every time an item was purchased, the user would quickly become frustrated with our app. In contrast, the table does discriminate between purchased and not purchased item by displaying the purchased and not purchased items in separate sections of the table. To accomplish this we created two new mutable arrays that will hold the indices of the items from the original list. Finally, we need two properties. One, the store we are to display, and the other is our custom built cell. ShoppingListViewController.h
#import #import #import #import <UIKit/UIKit.h> "Store.h" "ListItemViewController.h" "ListItemCell.h"

@interface ShoppingListViewController : UITableViewController <ListItemDelegate> @property (nonatomic, weak) Store *store; @property (nonatomic, weak) IBOutlet ListItemCell *listItemCell; @end

We will begin our examination of the ShoppingListViewController with some private methods. The first is setupDataModel, which simply populates our purchased and not purchased arrays with the indices of the list items in the store. This method will be called from several different places in response to a variety of actions, so the very first thing we need to do is clear both arrays. Then, we simply iterate through the collection of items, and store the index of each item in the appropriate array. Next we have the addItem method which will be called from the add button on the navigation bar. This is a standard method the likes of which we have seen many times before. ShoppingListViewController.m (section 1 of 6)
#import "ShoppingListViewController.h" @implementation ShoppingListViewController { @private NSMutableArray *notPurchased; NSMutableArray *purchased; BOOL autoDisplayed; } @synthesize store; @synthesize listItemCell; #pragma mark - Private Methods

Nicolaas tenBroek (AMDG)

373

iOS Development ShoppingListViewController.m (section 1 of 6)


- (void)setupDataModel { [purchased removeAllObjects]; [notPurchased removeAllObjects]; for(int i = 0; i < [store numItems]; i++) { ListItem *item = [store itemAtIndex:i]; if(item.isPurchased) { [purchased addObject:[NSNumber numberWithInt:i]]; } else { [notPurchased addObject:[NSNumber numberWithInt:i]]; } }

Accessibility

- (void)addItem { ListItemViewController *livc = [[ListItemViewController alloc] initWithNibName:@"ListItemViewController" bundle:nil]; livc.delegate = self; [self.navigationController pushViewController:livc animated:YES]; }

The next method is purchasedButtonPressed: and is a standard action method, but rather than wiring it up through Interface Builder we will be attaching it manually. Specifically we will be attaching this method as an action for the purchase toggle button that is on the table cell we created earlier. We will use this method to animate the movement of an item between the two table sections. If we were not concerned with animating the move, this method could be incredibly simple, but the animation will give the user visual feedback of the change. Without an animation the change would be sudden and the user would be forced to carefully scan the table to discover what had changed. The animation sequence we selected was to slide the cell out to the left, and then slide it into its new position from the left. Unfortunately the button does not send us a reference to the cell that contains it, so the first thing we need to do is navigating up the chain of super-views until we have a reference to the cell. When we have a reference to the cell that contains the button, we can ask the table for an indexPath to that cell. The indexPath will then tell us whether the item represented by that cell is in the purchased or not purchased array. We can then use the index from the appropriate array to toggle the state of the list item in the store. With the state modified we will need to rebuild our data model. Note that this is how we will handle keeping the items in their original location within the list. When an items state is toggled it moves between the purchased and not purchased sections of our table, but remains in the same location in the stores collection. So, an item that was marked as purchased will return to its original location in the list if it is later marked as not purchased. After rebuilding the model we need to find the indexPath to the new location of the item. With two indexPaths we can then ask the table to animate a deletion from the old index and insertion at the new index. The table can handle multiple cell deletions and insertions at the same time, so the indexPaths

Nicolaas tenBroek (AMDG)

374

iOS Development

Accessibility

need to be stored in arrays. It is worth noting here that the table will not begin any of the actions until it receives the endUpdates message. Upon receipt of that message it will first process the deletions and then the insertions, regardless of the order in which you asked for the changes. With the item moved into its new location we have another opportunity to provide useful feedback through VoiceOver. If there are still more items on the list to be purchased we ask VoiceOver to read the name of the next item, otherwise we inform the user that all of the items have been purchased. Just as we must try our best to take actions for the user whenever possible, we must also consider when notifications might save the blind user an additional interaction. ShoppingListViewController.m (section 2 of 6)
#pragma mark - Actions - (void)purchasedButtonPressed:(id)sender { UIView *view = nil; for(view = ((UIButton *)sender).superview; view && ![view isKindOfClass:[UITableViewCell class]]; view = view.superview); if(view) { NSIndexPath *oldIndexPath = [self.tableView indexPathForCell:(UITableViewCell *)view]; NSNumber *itemIndex = oldIndexPath.section? [purchased objectAtIndex:oldIndexPath.row] : [notPurchased objectAtIndex:oldIndexPath.row]; ListItem *listItem = [store itemAtIndex:[itemIndex intValue]]; listItem.isPurchased = !listItem.isPurchased; [self setupDataModel]; NSIndexPath *newIndexPath = nil; if(oldIndexPath.section) { for(int i = 0; i < [notPurchased count]; i++) { if([[notPurchased objectAtIndex:i] isEqualToNumber:itemIndex]) { newIndexPath = [NSIndexPath indexPathForRow:i inSection:0]; break; } } } else { for(int i = 0; i < [purchased count]; i++) { if([[purchased objectAtIndex:i] isEqualToNumber:itemIndex]) { newIndexPath = [NSIndexPath indexPathForRow:i inSection:1]; break; } } } [self.tableView beginUpdates]; NSArray *indexPaths = [[NSArray alloc] initWithObjects:oldIndexPath, nil]; [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];

Nicolaas tenBroek (AMDG)

375

iOS Development ShoppingListViewController.m (section 2 of 6)

Accessibility

indexPaths = [[NSArray alloc] initWithObjects:newIndexPath, nil]; [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft]; [self.tableView endUpdates]; if([notPurchased count]) { ListItem *item = [store itemAtIndex:[[notPurchased objectAtIndex:0] intValue]]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [NSString stringWithFormat:@"Next item is: %@", item.itemDescription]); } else { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"All items have been purchased."); }

} }

The next three methods are fairly straightforward. In setStore: we simply save the store, set the views title to the name of the store, and setup the tables data model. In our ListItemDelegate methods we need to treat the creation of an item separately from an update. When an item is created it has to be added to the store, we then need to rebuild the tables data model, and finally tell the table to reload. When a list item is updated we need only ask the table to reload, because nothing in the model will have changed. ShoppingListViewController.m (section 3 of 6)
#pragma mark - Custom Setters - (void)setStore:(Store *)aStore { store = aStore; self.title = store.name; [self setupDataModel]; } #pragma mark - ListItemDelegate - (void)listItemCreated:(ListItem *)newItem { [store addItem:newItem]; [self setupDataModel]; [self.tableView reloadData]; } - (void)listItemUpdated:(ListItem *)updatedItem { [self.tableView reloadData]; }

In the next section we have our standard UIViewController methods. In the init we simply setup the array of buttons for use with the toolbars, and create our data model arrays. Testing showed us that for

Nicolaas tenBroek (AMDG)

376

iOS Development

Accessibility

very long shopping lists the edit button on the toolbar could be accidentally hit when attempting to toggle the state of the last item on the screen as the checkmark button was immediately above the toolbars edit button. To solve that issue we pushed the edit button to the far right of the toolbar by preceding it with a flexible spacer. The flexible spacer works by consuming all the remaining width of the toolbar after accounting for the other buttons. You will note that the edit button used is the very handy built-in edit button provided by the UITableViewController class. In viewDidAppear: things are more interesting. We begin by dealing with the situation caused by an empty list. If the shopping list is empty (i.e. no purchased and no un-purchased items), we need to autodisplay the add item screen. The trouble is, if we simply checked for an empty list and then displayed the add item screen, a user who accidentally selected an empty list would be forced to enter an item just to get back to the home screen. Remember that viewDidAppear: runs each time the view is displayed, and that means it also runs when the user presses the back button from the add item view. We solve this dilemma with a Boolean variable. If the user hits back from the add item screen then we go ahead and display an empty list. Regardless of what is going to happen, we need to alert our blind users, which we do with a set of notifications, tailored for each scenario. The last thing we need to do in this method is to display the toolbar. Remember that it is hidden by default, and we have to hide it again when the screen goes away (unless all the screens in our app are using it), so that it only appears when needed. In viewWillDisappear: we must remember to hide the toolbar. We dont want any animation on either the appearance or disappearance of the toolbar because we want it to appear as a permanent part of this view. Therefore it should simply appear and disappear with the view, not as a separate item. ShoppingListViewController.m (section 4 of 6)
#pragma mark - UIViewController - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if ((self = [super initWithNibName:nibName bundle:bundle])) { purchased = [[NSMutableArray alloc] init]; notPurchased = [[NSMutableArray alloc] init]; UIBarButtonItem *spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; NSArray *editButtonArray = [[NSArray alloc] initWithObjects:spacer, self.editButtonItem, nil]; self.toolbarItems = editButtonArray; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addItem)]; [self.navigationController setToolbarHidden:NO animated:YES];

Nicolaas tenBroek (AMDG)

377

iOS Development ShoppingListViewController.m (section 4 of 6)


}

Accessibility

self.tableView.separatorColor = [UIColor colorWithRed:0.729 green:0.639 blue:0.47 alpha:1.0];

- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if(!autoDisplayed && ![notPurchased count] && ![purchased count]) { autoDisplayed = YES; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"List is empty, adding an item."); [self addItem]; } else { if([notPurchased count]) { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, [NSString stringWithFormat:@"First item is: %@", [[store itemAtIndex:[[notPurchased objectAtIndex:0] intValue]] itemDescription]]); } else if([purchased count]) { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"All items have been purchased."); } else { UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, @"List is empty."); } [self.navigationController setToolbarHidden:NO animated:NO]; }

- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.navigationController setToolbarHidden:YES animated:NO]; }

In the next section of code we list the UITableViewDataSource methods. The implementations of these methods are fairly standard. In fact, there are only a few noteworthy elements in the entire section. First in tableView:cellForRowAtIndexPath:, when creating a new cell instance we need to handle two important tasks. First we attach our action method purchasedButtonPressed: (from section 2 of this classs code) to the purchased button in the cell. You will recall that the button already has an action method that toggles the image displayed, and now we are attaching a second action that implements the real work of changing the state of the item. The next thing we do is to enable a disclosure button as the cells accessory. The disclosure button automatically calls the method tableView:accessoryButtonTappedForRowWithIndexPath: in the tables delegate, which means we will also need to implement that method. While the purchased button allows for toggling the state of the item, the disclosure button will allow the user to edit the item. The next interesting item in this section is in the tableView:commitEditingStyle:forRowAtIndexPath: method. We need only to implement the delete action, and we decided to use the fade action for the

Nicolaas tenBroek (AMDG)

378

iOS Development

Accessibility

delete. Keep in mind we already used the sliding out to the left action to animate an items change in state, so we want something that is visually quite different to indicate the deletion. The last item of note in this section involves rearranging the list. After some consideration we realised that logically only items that have not been purchased should be able to move their positions in the list. Therefore we implement moves only for that section, and after each move we need to rebuild our table model. ShoppingListViewController.m (section 5 of 6)
#pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if(!section) { return [notPurchased count]; } return [purchased count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { ListItemCell *cell = (ListItemCell *) [tableView dequeueReusableCellWithIdentifier:LIST_ITEM_CELL_IDENTIFIER]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"ListItemCell" owner:self options:nil]; cell = listItemCell; [cell.purchasedButton addTarget:self action:@selector(purchasedButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; cell.selectionStyle = UITableViewCellSelectionStyleNone; } ListItem *item = [store itemAtIndex:[((indexPath.section)? [purchased objectAtIndex:indexPath.row] : [notPurchased objectAtIndex:indexPath.row]) intValue]]; cell.itemLabel.text = item.itemDescription; cell.isPurchased = item.isPurchased; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

Nicolaas tenBroek (AMDG)

379

iOS Development ShoppingListViewController.m (section 5 of 6)

Accessibility

} }

if (editingStyle == UITableViewCellEditingStyleDelete) { int index = -1; if(indexPath.section) { index = [[purchased objectAtIndex:indexPath.row] intValue]; [purchased removeObjectAtIndex:indexPath.row]; } else { index = [[notPurchased objectAtIndex:indexPath.row] intValue]; [notPurchased removeObjectAtIndex:indexPath.row]; } [store removeItemAtIndex:index]; [self setupDataModel]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return !indexPath.section; } - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { [store moveItemAtIndex:[[notPurchased objectAtIndex:fromIndexPath.row] intValue] toIndex:[[notPurchased objectAtIndex:toIndexPath.row] intValue]]; [self setupDataModel]; }

In our last section for the table controller we handle two very important tasks. The first is to allow editing of list items in tableView:accessoryButtonTappedForRowWithIndexPath:. This method is very straightforward; it simply obtains a reference to the item displayed by the cell, creates a ListItemViewController, and sets up the view for editing with the current table as the delegate. The next method is a bit more interesting however. As we are only allowing rows in the first section to be rearranged, we need to visually indicate to the user that they are not allowed to move a row from the first section into the second. The second sections items will not display drag icons because we already indicated that only cells from the first section could be moved. Unfortunately, by default the table will allow you to drag a cell from the first section into the second one. We can fix that behaviour by overriding the method tableView:targetIndexPathForMoveFromRowAtIndexPath:ToProposedIndexPath:. In this method we simply return back the passed-in indexPath as long as the user tries to move the cell within the first section. As soon as they attempt to drag the item into the second section (i.e. the purchased section), we change the returned indexPath to list the last row in the first section. Recall that as you drag a cell around the screen the other cells move out of the way, creating an empty space where you can drop the cell you are dragging. By overriding this method, the empty space will always be at the bottom of the first section if the user drags the cell anywhere outside of the section. If they drop the cell while it is

Nicolaas tenBroek (AMDG)

380

iOS Development

Accessibility

over the second section the table will smoothly animate the cell into the empty space in the first section, so in those instances the user will see the cell move into its proper space. ShoppingListViewController.m (section 6 of 6)
#pragma mark - Table view delegate - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { ListItem *item = nil; if(indexPath.section) { item = [store itemAtIndex:[[purchased objectAtIndex:indexPath.row] intValue]]; } else { item = [store itemAtIndex:[[notPurchased objectAtIndex:indexPath.row] intValue]]; } ListItemViewController *livc = [[ListItemViewController alloc] initWithNibName:@"ListItemViewController" bundle:nil]; livc.item = item; livc.delegate = self; [self.navigationController pushViewController:livc animated:YES]; } - (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { if(sourceIndexPath.section == proposedDestinationIndexPath.section) { return proposedDestinationIndexPath; } int newRow = 0; if(sourceIndexPath.section < proposedDestinationIndexPath.section) { newRow = [self tableView:self.tableView numberOfRowsInSection:sourceIndexPath.section] - 1; } } @end return [NSIndexPath indexPathForRow:newRow inSection:sourceIndexPath.section];

With our code complete, it is time to test the app. Unfortunately the simulator does not support VoiceOver, which means you can only completely test the accessibility features of the app by using a real device. You must not forget to use the Screen Curtain while testing! Of course, the reason the simulator cannot support accessibility is that the accessibility features all require multi-touch, and that is very hard to simulate accurately. Fear not though! In place of VoiceOver the simulator provides an Accessibility Inspector. The inspector gives on-screen indications of the information that will be provided to VoiceOver, which means we can do a quick visual verification before testing the app with

Nicolaas tenBroek (AMDG)

381

iOS Development

Accessibility

VoiceOver. To enable the inspector, launch the simulator and then open the Settings app. Go to the General section and then choose Accessibility. Note that this is the same location for enabling VoiceOver on a real device. Once there, you can simply turn on the inspector.

As you can see, the inspector presents a rainbow coloured dialogue that displays the information about the current screen or item. Here is a screenshot of the inspector in our app on the Create Store view:

Nicolaas tenBroek (AMDG)

382

iOS Development

Accessibility

If the inspector gets in the way you can move it about by grabbing the title bar of the dialogue and dragging it around. You can also minimise it and expand it again by clicking on the x button. Here is the same screen with the inspector minimised:

And there you have it, a complete app that is useful for both the blind and the sighted. We suggest you try using it for a while with Screen Curtain enabled. See if you can create a couple of new stores and fill those lists with real items you need. Try rearranging the list to reflect your shopping habits. After all, the only way for you to begin to understand how the blind will use your app is to do it yourself. If you think something needs to be changed, then by all means, make the change and try it out. Just remember that if you cannot do it without peeking, it is likely implemented incorrectly.

Nicolaas tenBroek (AMDG)

383

iOS Development

Accessibility

Nicolaas tenBroek (AMDG)

384

iOS Development

Sensing

Sensing
The iOS platform provides a number of mechanisms for detecting user interactions. Up until this point we have been mostly ignoring those mechanisms and simply accepting that user input happened almost magically. Obviously it is possible to write an entire app without needing anything more than the builtin mechanisms for detecting user interaction, but utilising those mechanisms to create custom interface mechanics can make for a much richer experience for the users. Much of the code for handling user input is actually housed in the class UIResponder, which is the parent class for UIApplication and UIView. Those two classes are the parents of virtually everything the user interacts with in an app, which makes UIResponder a very important class indeed. There are three basic categories of events that can be handled through UIResponder. They are: touch events, motion events, and remote control events. While the first two categories are named well enough to be selfexplanatory, the latter is a bit oddly phrased. The term remote here is used to indicate any external accessory, which can be a Bluetooth, WiFi, or plugged in device capable of sending input signals. For instance, a remote-control event is sent when the volume button is pressed on a plugged-in headset, or the answer button is pressed on a Bluetooth earpiece.

Touch
We will begin our investigation of detecting user input with the most basic element: touch. Touch detection should not be confused with gesture recognition, as it is a much more primitive level of detection than gestures are. A gesture is a pre-defined set of actions that can be a set of touches or device movements, or even a combination of those two, whereas a touch is a simple random placement of one or more fingers on the screen. UIResponder provides four methods for processing touches: touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:WithEvent:. Each method receives as arguments a set of UITouch instances and a UIEvent instance. The set of touches will contain a separate object for each point of contact (i.e. finger) on the screen. The event object encapsulates information about the event in general like the general kind of event and the timestamp when it occurred; whereas the touch objects each contain information about the specific point of contact. So, to handle a users touch events directly, one needs only to override the methods that inform about the events one is interest in. We will demonstrate the use of those methods via a simple app that allows the user to draw on the screen with their finger. In the interest of clarity we will restrict our example to processing single-touch events, but it could be easily extended to handle multiple touches. Begin by creating a Single View Application named TouchDraw. For this app we will need to override the UIVew class, which will allow us to draw directly to the screen. We have not yet seen an example of overriding the UIView, but it is fairly straightforward process. Begin by adding a new Objective-C class called TouchDrawView, and be sure to indicate that it is a subclass of UIView in the class creation wizard. We will write the code for this class momentarily, but right now we need to ensure that our version of the view is the one used by the controller. So, open up the ViewControllers nib file in Interface Builder. With the view selected open the Identity Inspector and expand the Custom Class section. That section contains a single drop-down selector titled Class. Choose TouchDrawView from the list (or type it if you

Nicolaas tenBroek (AMDG)

385

iOS Development

Sensing

prefer), and save the change. This is the only change we will need to make through Interface Builder; all of our other changes will be in code. Next we need another new class, but this will be a straightforward subclass of NSObject. Name the class TouchPoint. This class will simply hold the vertex of a touch in the views coordinate system. We will need two float values (x and y) exposed through read-only properties, and an init method that accepts those values as arguments. This is nothing more than a data class, which will lead to questions about its usefulness. Yes, we can do without this class, but the example will be a bit simpler with it in place, and clarity is our goal here, not efficiency. TouchPoint.h
#import <Foundation/Foundation.h> @interface TouchPoint : NSObject @property (nonatomic, readonly) float x; @property (nonatomic, readonly) float y; - (id)initWithX:(float)xVal andY:(float)yVal; @end

TouchPoint.m
#import "TouchPoint.h" @implementation TouchPoint { @private float x; float y; } @synthesize x; @synthesize y; - (id)initWithX:(float)xVal andY:(float)yVal { if((self = [super init])) { x = xVal; y = yVal; } return self; } @end

Nicolaas tenBroek (AMDG)

386

iOS Development

Sensing

With our data class in place we can turn our attention back to the view itself. Each line to be drawn on the screen will be stored as an array of TouchPoint instances, so the view will require a method for adding a new line. TouchDrawView.h
#import <UIKit/UIKit.h> @interface TouchDrawView : UIView - (void)addLine:(NSArray *)newLine; @end

The first section of our implementation is quite simple. We create an array to store the individual lines and implement the method that allows new lines to be added to the collection. TouchDrawView.m (section 1 of 2)
#import "TouchDrawView.h" #import "TouchPoint.h" @implementation TouchDrawView { @private NSMutableArray *lines; } - (id)initWithCoder:(NSCoder *)decoder { if((self = [super initWithCoder:decoder])) { lines = [[NSMutableArray alloc] init]; } return self; } - (void)addLine:(NSArray *)newLine { [lines addObject:newLine]; }

The second section of the implementation is a bit more complicated. Here we override the method drawRect: which should only be done in cases where you want direct control over the screen drawing. Most of the time this method should be left alone as even empty implementations can adversely impact application performance. We begin this method by initialising the colour components to black, and then set the line width to five pixels. Then, for each line in the lines array we draw a new line on the screen. The first line we draw in black, and then modify the colour before drawing the next line. To make the colour changes more dramatic, we alternate the changes between the red, green, and blue fields, and calculate a new colour based on the previous lines length. This means that a short line will result in the next line being a very similar colour, and a long line will result in the next line being very Nicolaas tenBroek (AMDG) 387

iOS Development

Sensing

different. You will notice that after each change is calculated we have a rather odd line of code similar to this:
red -= (int)red;

The purpose of those statements is to keep the colour values within the valid ranges. Colours range from 0.0 to 1.0. Anything over 1.0 is simply treated as 1.0 which would mean that after a number of lines were drawn the colour would be stuck on white (i.e. all three colour values would eventually be greater than 1.0). Subtracting the int value simply keeps the values below 1.0 while cycling colours endlessly. TouchDrawView.m (section 2 of 2)
- (void)drawRect:(CGRect)rect { //colour components CGFloat red = 0.0f; CGFloat green = 0.0f; CGFloat blue = 0.0f; CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetLineWidth (ctx, 5.0f); for(int i = 0; i < [lines count]; i++) { NSArray *line = [lines objectAtIndex:i]; //move to starting location for line TouchPoint *touchPoint = [line objectAtIndex:0]; CGContextMoveToPoint(ctx, touchPoint.x, touchPoint.y); CGContextSetRGBStrokeColor(ctx, red, green, blue, 1.0); //add points on the line for(int j = 1; j < [line count]; j++) { touchPoint = [line objectAtIndex:j]; CGContextAddLineToPoint(ctx, touchPoint.x, touchPoint.y); } //draw the line CGContextDrawPath (ctx, kCGPathStroke); //change colour of next line CGFloat changeAmount = 1.0f / 256.0f * [line count]; switch(i % 3) { case 0: red += changeAmount; red -= (int)red; break; case 1: green += changeAmount; green -= (int)green; break; case 2: blue += changeAmount; blue -= (int)blue;

Nicolaas tenBroek (AMDG)

388

iOS Development TouchDrawView.m (section 2 of 2)


} } @end }

Sensing

With the view complete we can (finally) begin to focus on capturing the users touch. The touch handling is best carried out in the view controller. Thankfully this code will be almost impossibly simple to implement. As the view itself is keeping track of the line history, we need only one array to store the points on the line the user is currently drawing. We begin our implementation with a helper method called addTouchToLine:. This method takes the UITouch argument and converts it to a point in the views coordinate system with a call to locationInView:. With the proper coordinates set we can create a TouchPoint object, add it to the current line, and then inform the view that it needs to update itself. We cannot call the drawRect: method directly, instead we must only inform the view that it needs to redraw itself. It is then up to the view to decide the appropriate time to perform the redrawing. ViewController.m (section 1 of 2)
#import "ViewController.h" #import "TouchDrawView.h" #import "TouchPoint.h" @implementation ViewController { @private NSMutableArray *currentLine; } #pragma mark - Touch Handling - (void)addTouchToLine:(UITouch *)touch { CGPoint point = [touch locationInView:self.view]; TouchPoint *touchPoint = [[TouchPoint alloc] initWithX:point.x andY:point.y]; [currentLine addObject:touchPoint]; [self.view setNeedsDisplay]; }

Next we have the overridden touch handling methods. In touchesBegan:withEvent: we create a new line, add the current touch to the line, and then add the new line to the view. It may seem odd to add the line to the view at this juncture; after all it contains only a single point. However, if we waited to add the line to the view until the line was completed, the user would not be able to see it being drawn. The user would drag their finger around with no apparent result. Then, when they lifted their finger the line would magically appear. If you would like to compare the results of these two approaches, simply move the add line statement to touchesEnded:withEvent: and try running the app again.

Nicolaas tenBroek (AMDG)

389

iOS Development

Sensing

In touchesEnded:withEvent: we add the last point to the line, and then release the line. This method is called when the user lifts their finger from the screen so we no longer need the reference to the line. The next touch will create a new line, so it is important to release the line here to avoid any memory issues or accidental changes to a completed line. The method touchesMoved:withEvent: is called in between the beginning and ending touch methods. Each time the user moves their finger, that move is recorded through a call to this method. Notice that in each method we use the rather amusingly named anyObject method. We are using this method for two reasons. First, the touches come in as a set, so no ordering is imposed. Second, as we are supporting only a single touch interface, the anyObject method will return to us the one touch instance in the set without going through the trouble of iteration. Obviously if we were handling more than one touch, we would be forced to iterate through the set. We are handling the cancel touch event in our code just as if it were an end event. Generally this is not a good idea. Cancel events happen when something interrupts the current app like a phone call or alert popup, but those are not the only sources of cancel events. In the section on gestures we will discuss the conflicts between gestures and touch handling which will highlight the danger of the approach we have taken in this example. Frankly, in virtually every instance of a cancel event you should actually cancel the current process, which in this case would mean deleting the current line. Deleting an already drawn just because someone called seems a bit cruel, so we simply decided to end the line at its current point. ViewController.m (section 2 of 2)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { currentLine = [[NSMutableArray alloc] init]; [self addTouchToLine:[touches anyObject]]; [(TouchDrawView *)self.view addLine:currentLine]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self addTouchToLine:[touches anyObject]]; currentLine = nil; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self addTouchToLine:[touches anyObject]]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } @end

Nicolaas tenBroek (AMDG)

390

iOS Development

Sensing

Those changes complete our code. Running it allows us to demonstrate our finger painting abilities (or extreme lack thereof).

As silly as it may seem, this app is actually a nearly complete drawing platform. It would be a trivial enough matter to add a colour selector to the bottom of the screen and create a custom class for the line storing the colour information and line size with the points. That being said, such changes are beyond the scope of our needs at the moment, so we shall leave them as an exercise for the reader. Multiple Touches Handling multiple touches is not really much different from handling single touch events. In fact, implementing multi-touch handling in our drawing app would really be quite simple. We would begin by replacing our single array of points with a dictionary. When handling multi-touch there is no way to know ahead of time how many touches you will receive at any given time, so you must be prepared to handle all of them. Additionally, there is no way to know when any of the current touches will change, so no ordering of touch events can be assumed. A dictionary is a very good solution to this massive uncertainty as it too is not an ordering mechanism, but the key offers a definitive way to differentiate objects. We simply need to assign each line a unique key value, and then we can reliably retrieve the line we need. In our single touch example the array we were using to store the points was created and destroyed constantly, so we had no use for an init method in our controller. In this modified version, our dictionary must be alive for the entire course of the app, which necessitates an init, simple though it may be. Additionally, each view is created in single-touch mode by default. Therefore we must specifically enable multi-touch mode for each view that needs it. In the code below we are enabling

Nicolaas tenBroek (AMDG)

391

iOS Development

Sensing

multi-touch as soon as the view has been loaded. In single touch mode the view simply ignores all touch events after the first one, and the app is never informed of the existence of those events. ViewController.m (section 1 of 2)
#import "ViewController.h" #import "TouchDrawView.h" #import "TouchPoint.h" @implementation ViewController { @private NSMutableDictionary *currentLines; } #pragma mark - View Controller Lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if((self = [super initWithNibName:nibName bundle:bundle])) { currentLines = [[NSMutableDictionary alloc] init]; } } return self;

- (void)viewDidLoad { [super viewDidLoad]; self.view.multipleTouchEnabled = YES; }

One of the issues you must deal with when handling multiple touches is how to know which current touch is related to which previous touch. Lets imagine a situation where the user places two fingers on the screen. We receive two touch objects, one for each finger. The user then spreads his fingers apart, and we begin receiving touchesMoved events. Each event notification provides a set of two touches, but as the set imposes no ordering we have no idea which touch event belongs to which finger. We need a way to relate the moved touches to the original touches in order to discover the direction and distance of the moves. In that situation the solution is actually quite simple. Each touch object provides not only its current location in the view (which we have retrieved with the method locationInView:), but also its previous location in the view. In our current case though, we need more than just the previous location of a given touch. We need an object to serve as the key for our dictionary. Unfortunately, the locations are returned as CGPoint structures, which cannot serve as objects. It turns out though, that a UITouch instance is created when a new touch begins, and that same touch object is then used for the entire life of the touch. In other words, that same touch object you receive in touchesBegan:withEvent: is passed to the touchesMoved:withEvent: and touchesEnded:withEvent: methods. Such a long life makes the objects attractive prospects for key values. Unfortunately, the UITouch API expressly prohibits you from calling the retain method on any UITouch instance which means it cannot be added to any collection. For this

Nicolaas tenBroek (AMDG)

392

iOS Development

Sensing

throwaway example code we decided to solve the problem by turning each objects memory address into an NSNumber instance, and use that for the key. This is not simply a poor solution; it is also dangerous and should not be used in any production code. Our decision to use the address here was made purely in the interests of expediency and simplicity. Our goal was to implement this example with as little change to the previous code as possible. Please do not take the following code as endorsement of this approach. In later versions of iOS Apple is sure to implement a garbage collector, and that subsystem will surly expose the weaknesses of any code using this approach. Additionally, in long-lived apps objects move around in RAM whenever RAM is defragmented, and therefore the address of an object cannot be considered constant. Even worse, apps on mobile devices are often suspended and resumed, and the memory locations of the objects can be changed each time that happens. Now that the proper warning has been issued, we can examine the changes to the code, though in actuality those changes are quite few. In addTouchToLine: we modified only the line of code that stores the point. Rather than directly accessing the currentLine array, we now retrieve the array from the dictionary and then add the point to it. Otherwise this code is exactly the same as we had in the previous example. Similar changes are in place in the other touch event methods. In addition, each of those methods has replaced the anyObject method call with a fast iteration loop that processes all the touches received. At this point our project is ready to run. Now you can draw with as many fingers as you can fit on the screen. ViewController.m (section 2 of 2)
#pragma mark - Touch Handling - (void)addTouchToLine:(UITouch *)touch { CGPoint point = [touch locationInView:self.view]; TouchPoint *touchPoint = [[TouchPoint alloc] initWithX:point.x andY:point.y]; [[currentLines objectForKey:[NSNumber numberWithLong:(long)touch]] addObject:touchPoint]; [self.view setNeedsDisplay]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for(UITouch *touch in touches) { NSMutableArray *currentLine = [[NSMutableArray alloc] init]; [currentLines setObject:currentLine forKey:[NSNumber numberWithLong:(long)touch]]; [self addTouchToLine:touch]; [(TouchDrawView *)self.view addLine:currentLine]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for(UITouch *touch in touches) { [self addTouchToLine:touch]; [currentLines removeObjectForKey:[NSNumber numberWithLong:(long)touch]]; } }

Nicolaas tenBroek (AMDG)

393

iOS Development ViewController.m (section 2 of 2)


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { for(UITouch *touch in touches) { [self addTouchToLine:touch]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } @end

Sensing

If you make these changes to your app you will notice that the colours change in a different way than they did before. Remember that our drawing code was based on the assumption that one line would be drawn at a time, and that once complete, it would never change its length. That assumption no longer proves true, so as you draw multiple lines, the lines will change colour each time they get longer. We will leave fixing that problem as an exercise for the reader. Good luck.

Nicolaas tenBroek (AMDG)

394

iOS Development

Sensing

Motion Events
Processing motion events is fairly similar to processing touch events. Like touch events, motion events are handled through the UIResponder class, so you need to override the methods which correspond to the events you wish to receive, just as we did before. There is one slight change to the process though. Motion events are only delivered to objects that have registered themselves as the first responder. You will recall from our many uses of text fields that registering those components as first responders causes the keyboard to display and all input to be directed to the component. We have not yet used the first responder with any other component, but any component that extends UIResponder has the capability to become a first responder. Unfortunately the default setup actually disables this ability, so you cannot simply call the becomeFirstResponder method on your controller and have things work out. By default, that method will have no effect. In order to allow a component to become a first responder the components class must override the method canBecomeFirstResponder so that it returns YES. With that change in place we can then implement the motion event methods. These are very similar methods to the touch events, though there are fewer of them. You can receive motion began, motion ended, and motion cancelled events. It may seem odd to cancel a motion, but the shake motion is designed to be a short fast shake (i.e. something deliberate). If the device starts moving the motion began event is generated, and then if it stops quickly (i.e. less than a second) the motion-ended event is generated. If the movement continues for too long, then the cancelled event is generated. As a short example of processing motion events we will modify our single-touch drawing code so that a shake motion clears the screen. We will need very small changes to our code, and will present only the changes here. In the views header file we add a method signature for the clear method (the change is in bolded font). TouchDrawView.h
#import <UIKit/UIKit.h> @interface TouchDrawView : UIView - (void)addLine:(NSArray *)newLine; - (void)clear; @end

Nicolaas tenBroek (AMDG)

395

iOS Development

Sensing

Next, add a very simple implementation of the clear method. We need only to remove all the lines from the history, and indicate that the view is ready to be redrawn. TouchDrawView.m
- (void)clear { [lines removeAllObjects]; [self setNeedsDisplay]; }

Finally, in the controllers implementation we need to add three methods. A component cannot become first responder until it is displayed on the screen, so we must override the viewDidAppear: method and from there call becomeFirstResponder. As we mentioned earlier, every component that wishes to become a first responder must override the canBecomeFirstResponder method so that it returns YES, so that code is presented as well. Finally, we are interested only in the ending motion event. If the event is cancelled it should have no effect, and we did not initiate any code in response to the motion began event, so we have nothing to cancel. When the motion ends we simply remove the current line (if one exists), and then ask the view to clear itself. TouchDrawViewController.m
#pragma mark - View Lifecycle - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self becomeFirstResponder]; } #pragma mark - UIResponder - (BOOL)canBecomeFirstResponder { return YES; } #pragma mark - Shake Handling - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { currentLine = nil; [(TouchDrawView *)self.view clear]; }

Truly, this is a very simple change, but also very effective. We can now easily clear the screen and did not need to add a control to our interface design. This might be a good place for an animation though, with a nice smooth transition from drawing to blank screen that informs the user of the change. The

Nicolaas tenBroek (AMDG)

396

iOS Development

Sensing

way it works at the moment, our sudden clearing of the screen could be interpreted as a bug. Considering the scope of this example, we will leave that animation as an exercise for the reader as well.

Nicolaas tenBroek (AMDG)

397

iOS Development

Sensing

Gestures
Gestures are in many ways related to touches; after all, the vast majority of gestures are simply sequences of touch events. Beginning in version 3.2 of iOS gestures became a separate type of item, and now are handled independently of a views touch handling system. This separation generally makes things much smoother for the app developer. Gesture code can now be packaged into a class, and thus can be reused whenever needed. Additionally, gesture code is no longer mixed in with the other touch handling code needed by an app which both better fits the rules of Object Oriented Design and helps programmers keep their sanity while producing better code. Almost any kind of user input can be turned into a gesture, but iOS supplies recognisers for a few common gestures. Those supplied gestures are: taps, pinches, rotations, swipes, pans, and long presses. Anything else you might need requires a custom subclass of UIGestureRecognizer. Most of the time, subclassing UIGestureRecognizer is a fairly straightforward matter. You simply override the necessary methods, observe the sequence of events, and decide whether or not a gesture occurred. In cases where gestures are used and the view also wants to process touch events things can get a bit more complicated. By way of example, imagine we add a gesture to our drawing app that takes a screenshot of our art. We decide that the screenshot gesture should be a fast diagonal swipe and must cover at least half the screen to be recognised. While the gesture recogniser is watching the touch events, waiting to see if it turns into the proper swipe, those same touch events are being delivered to the view, which is drawing a line. When the gesture is recognised, the system must inform the view of the gestures occurrence and also deal with all the touch events that should not have been sent to the view in the first place. The typical process to handle this situation is cancelling the touches sent to the view. Of course, we failed to properly handle the cancellation of touches (recall that we treated cancel and end as the same event), so our app would need to be redesigned to handle the cancel event by deleting the current line thereby ensuring the screenshot did not have an ugly streak across the image. To fully comprehend this process, we must examine the behaviour of gesture recognisers. While a gesture recogniser (GR) is processing touches it transitions between different states, and those states determine much of how the system handles the touch events given to other objects within the app. A GR spends most of its time in the possible state. Possible here literally means: this event may or may not be a gesture, and more data is needed to make a determination. The GR is in the possible state even when no events are being produced. When the GR decides to move itself out of the possible state, it must enter either began, failed, or recognised state, and does so according to the following rules: If events are being produced and the GR determines that those events do not compromise a gesture, the GR enters the failed state. Failed simply means that the system can continue delivering the events to any other object that may be listening to them. For discrete gestures (e.g. a three-finger single-tap), the GR will move directly from possible to failed or from possible to recognised.

Nicolaas tenBroek (AMDG)

398

iOS Development

Sensing

For continuous gestures (e.g. the screenshot swipe we discussed earlier), the situation is more complicated. The GR must enter the began state and then keep track of the events. Each time a new event is delivered the GR must examine the event and enter the changed state. o If the GR eventually determines that the gesture was not what it appeared to be, (for instance, if the swipe was too short or changed directions) then the GR must transition to the cancelled state. o If the events finally complete the necessary sequence to complete the gesture, then the GR must enter the ended state.

When a GR enters either the recognised or ended state, other touch listeners receive a cancelled event, and they should then undo any action taken since the touchesBegan event. In order to prevent overloading views with touch events that they may need to cancel, all touch events are first delivered to the GRs and then to the interested views. The system may even delay the delivery of some touch events, holding them briefly to see if anything else the GR might be interested in comes along. If no other interesting events happen then the delayed events are delivered to the interested views. For discrete gestures, the GR will recognise the gesture immediately and the events will never be delivered to a view. For multi-touch continuous gestures, things are a bit more complicated. By default, if a GR is in the began or changed states the system will delay delivering touchesEneded events to the views. GRs can also choose to delay the delivery of touchesBegan events if necessary. The reason for this delay may not be obvious, so lets return to our imaginary screenshot swipe. This time however, we shall imagine that we implemented it as a two-finger diagonal swipe to reduce the chances of confusion when drawing. The user decides to take a screenshot and so places two fingers on the screen in close proximity to each other. Our GR sees the double touch and starts tracking the movements out of interest. At this point the state remains possible because we cannot be sure this will be a swipe at this stage. For instance, the touch points may diverge rather than following one another, and that could not be considered a swipe. Because the GR is still in the possible state, the view is then notified of the touchesBegan events. As the user moves his fingers across the screen the GR decides this could well be a swipe and transitions into the began state. At this point the view receives a cancelled message and no more events are delivered to it. Then as more events come in the GR resets its state to changed repeatedly. The user reaches the end of the swipe and begins lifting his fingers from the screen. The chances that both fingers leave the screen at the exact same moment in time are almost impossibly remote, so the most likely event is that one finger is lifted and a touchesEnded event is created while the other finger remains in contact with the screen. We do not yet recognise the gesture because the entire sequence of events has not completed. So, rather than irrevocably destroying the touch events, the system simply delays delivering them. When the other finger leaves the screen (and another touchesEnded event is generated) our GR can then either recognise the gesture or not. If the gesture is not recognised, the delayed touch events are delivered to the view. If the gesture is recognised (i.e. the GR enters the ended state), then the touch events are discarded, and the view does not receive them.

Nicolaas tenBroek (AMDG)

399

iOS Development

Sensing

When implementing a custom GR you will also have access to the ability to delay the delivery of touchesBegan events. By default this delay is disabled, and you should consider the situation seriously before enabling it. The potential downfall of delaying the touchesBegan events is that your UI could respond sluggishly when the touch events appear to be the beginning of a gesture, and then turn out not to be one. For instance, if we delayed the touchesBegan events in our imaginary two-finger swipe gesture and the gesture ultimately fails (i.e. we decide the user is drawing with two fingers and not swiping), the lines would not appear on the screen until the failure happened. In the meanwhile, the user might think the system is not tracking those touches for some reason, lift their fingers and try again, which would ultimately ruin their drawing. Obviously it would be far easier for us to undo an unnecessary drawing event when a gesture is recognised than it would be for the user to correct their drawing after ruining it. (Yes, yes, if we allowed only single finger drawing, then we would simply draw nothing in response to a two-fingered touch, thereby eliminating the issue altogether. The point is that it is usually much easier to properly handle a touchesCancelled event than it is to deal with the user not understanding what is going on in the app and then performing another action as a result of that confusion.) Gesture recognisers have one more way in which they can control delivery of events. We mentioned earlier that when a gesture enters the began state the view receives a touchesCancelled event. If appropriate we can decide to let the view continue receiving events even while we are processing them in the GR. Simply set the GR property cancelsTouchesInView to NO, and all the touch events will continue to be delivered as normal. This would be appropriate in situations where the gesture might do something other than directly affect what is happening on the screen and the screen changes would need to continue as well. Obviously those situations are unlikely, so this property is set to YES by default. Now that we know how gestures work, we can implement one. Rather than implementing the screenshot double-fingered swipe we imagined earlier, we have decided to return to the flick gesture that we used in the chapter on animations. This gesture is a bit simpler than the swipe would be, and will more clearly demonstrate how to create a custom gesture recogniser. When the UIGestureRecognizer class was written, the designers did not consider the possibility that it might need to be sub-classed in the future (remember that the ability to add custom GRs did not appear until iOS 3.2). The rather unfortunate design decisions made during that time did not even account for the future public revealing of the class, which means we must begin our GR by importing UIGestureRecognizerSubclass.h rather than UIGestureRecognizer.h. If you do not import the subclass version of the header file, you will not have write access to the GRs state. Additionally, you absolutely must remember to call the super class version of every method you decide to override. Failure to pass those messages up the chain could make your app quite unstable. (There is a wonderful design lesson in this mess: ALWAYS assume that your code will be modified at a future date, and that in the future someone may need to subclass the class you are writing for reasons you have not yet thought of. Following proper OOD principles will make that future persons life much easier, and that future person may well be you too.)

Nicolaas tenBroek (AMDG)

400

iOS Development

Sensing

We will not be adding special unique methods to our subclass, so the entire interface is quite simple. Gesture recognisers must implement the same touch handling methods that views implement when they wish to process touch events. Remember that the same events are delivered to the GRs as are delivered to the views, so obviously the methods must match as well. All of these methods are inherited, so we have nothing else to add to the header file. FlickGestureRecogniser.h
#import <Foundation/Foundation.h> #import <UIKit/UIGestureRecognizerSubclass.h> @interface FlickGestureRecogniser : UIGestureRecognizer @end

Our flick GR will be similar to most other GRs in that it will be time restricted, and really will not need to hold much data, so we need only a few instance variables. We will define our flick gesture as a singletouch event in which the finger moves very quickly along a relatively straight and short line. Therefore, in our touchesBegan:withEvent: method we start by ensuring only one finger is on the screen. If more than one finger is present, then we transition directly to the failed state. If only one finger is touching we record the first touch point and timestamp of the first touch. We need those pieces of information because we have two different criteria to meet (fast and relatively straight). We then change the GRs state to began and finally pass the call up to the super class. FlickGestureRecogniser.m (section 1 of 5)
#import "FlickGestureRecogniser.h" @interface FlickGestureRecogniser(FlickGestureRecogniserPrivate) - (BOOL)slopesMatch:(UITouch *)touch; @end @implementation FlickGestureRecogniser { @private int touchCount; NSTimeInterval startTime; CGPoint firstPoint; CGPoint secondPoint; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if([[event allTouches] count] != 1) { self.state = UIGestureRecognizerStateFailed; } else { touchCount = 1; UITouch *touch = [touches anyObject]; firstPoint = [touch locationInView:self.view]; startTime = touch.timestamp; self.state = UIGestureRecognizerStateBegan;

Nicolaas tenBroek (AMDG)

401

iOS Development FlickGestureRecogniser.m (section 1 of 5)


} [super touchesBegan:touches withEvent:event]; }

Sensing

Next we handle move events. If the GRs state is not began or changed we know we can safely ignore these events, so we check the state first. If we are in a valid state we next examine the amount of time that has passed since the event began. If the event has been in progress for more than 200 milliseconds, then we can assume that this is not a flick and enter the failed state. Why 200 milliseconds? There was nothing scientific behind that number to be sure. That speed felt comfortable when we wrote the app, but definitely needs to be tested on multiple people (and some actual human factors research would not hurt either). After we have satisfied the speed requirement, we can look at the touches. If this event is providing the second touch data point, then there is not too much we can do other than record the location and enter the changed state. Once we have more than two data points we can begin to examine the line being created. Each time a move comes in we check the slope between the start point and the new point and compare that to the slope of the original line. If the slopes match then we enter the changed state again. If the slopes do not match, then we know the users are doing something other than flicking and the GR should enter the failed state. FlickGestureRecogniser.m (section 2 of 5)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if(self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) { //ensure gesture is fast if(([[touches anyObject] timestamp] - startTime) > 0.2) { self.state = UIGestureRecognizerStateFailed; } else { //ensure gesture is reasonably straight if(++touchCount == 2) { secondPoint = [[touches anyObject] locationInView:self.view]; self.state = UIGestureRecognizerStateChanged; } else { self.state = ([self slopesMatch:[touches anyObject]])? UIGestureRecognizerStateChanged : UIGestureRecognizerStateFailed; } } } [super touchesMoved:touches withEvent:event]; }

Nicolaas tenBroek (AMDG)

402

iOS Development

Sensing

When the touches have ended we need to decide whether or not a flick happened. If the GR is still in the changed state we look at the number of data points received. We decided to implement a requirement that more than two data points were required to ensure meant a flick happened. The reasoning behind this is simple: no person can hold absolutely still. Any single touch that lasts longer than a tap is followed by very small movements, which should not be interpreted as intentional. Frankly, the touchCount is a fairly inaccurate mechanism. Forcing a specific length requirement would be a much more accurate approach, but we opted for the simple and rough tactic in this example. (Again, this is a place where actual research would be helpful.) If all of the requirements are satisfied, then we enter the ended state, otherwise the gesture fails. The ended and recognised states are equivalent, but by convention the recognised state is used for discrete gestures, and ended for continuous ones. FlickGestureRecogniser.m (section 3 of 5)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if(self.state == UIGestureRecognizerStateChanged && touchCount > 2) { self.state = UIGestureRecognizerStateEnded; } else { self.state = UIGestureRecognizerStateFailed; } [super touchesEnded:touches withEvent:event]; }

Next we handle the touchesCancelled event by failing the gesture recognition and setting the number of touches back to zero. FlickGestureRecogniser.m (section 4 of 5)
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { touchCount = 0; self.state = UIGestureRecognizerStateFailed; [super touchesCancelled:touches withEvent:event]; } - (void)reset { touchCount = 0; [super reset]; }

Finally, we have our private helper method that we used to check the slopes created by the different points. You will recall that we saved the first two points when the gesture began, and we will be comparing all other movements against the slope of the line created by those two points. Again, we are dealing with people here and no person can drag their finger in a perfectly straight line, so we allow some variance in the movement. Trial and error brought us the acceptable deviation of 400, but as with

Nicolaas tenBroek (AMDG)

403

iOS Development

Sensing

the touch speed, this really would benefit from some empirical research and trials with several different kinds of users. FlickGestureRecogniser.m (section 5 of 5)
#pragma mark - private Methods - (BOOL)slopesMatch:(UITouch *)touch { //ensure slopes are reasonably close to each other CGPoint anotherPoint = [touch locationInView:self.view]; return fabs((firstPoint.y - secondPoint.y) * (firstPoint.x - anotherPoint.x) (firstPoint.y - anotherPoint.y) * (firstPoint.x - secondPoint.x)) <= 400; } @end

With the gesture recogniser in place, we can turn our attention to using it. Rather than using a delegate, the recognisers allow you to use a selector specify the method to be called whenever the GR changes state. The selector method must use one of the following signatures:
- (void)gesture_Handing_Method_Name - (void)gesture_Handling_Mehtod_Name:(UIGestureRecognizer *)gestureRecogniser

Obviously the method name can be any name you wish, and you can make the argument match the exact type of GR that you expect to receive. When creating these methods you should keep in mind here that Objective-C does not allow method overloading. If you are using several GRs, you will need to use unique names for each handling method. Once the view is ready, you can create and register the GR, and set up your controller (or any other object) as the receiver of the GR messages. Unfortunately the selector method you identified will be called each time the GR changes state, so you will most likely need to examine the GR and make decisions about your actions based on its state. As an example, here is a bit of the code we developed in the chapter on Animations to use the FlickGestureRecogniser. FlickGestureViewController.m (partial listing)
#pragma mark - View Lifecycle - (void)viewDidLoad { [super viewDidLoad]; FlickGestureRecogniser *flickGesture = [[FlickGestureRecogniser alloc] initWithTarget:self action:@selector(handleFlickGesture:)]; [self.view addGestureRecognizer:flickGesture]; [flickGesture release], flickGesture = nil; } #pragma mark - Handle Gestures - (void)handleFlickGesture:(FlickGestureRecogniser *)flickGestureRecogniser {

Nicolaas tenBroek (AMDG)

404

iOS Development FlickGestureViewController.m (partial listing)


switch(flickGestureRecogniser.state) { case UIGestureRecognizerStateBegan: startPoint = [flickGestureRecogniser locationInView:self.view]; break; case UIGestureRecognizerStateEnded: endPoint = [flickGestureRecogniser locationInView:self.view]; UIImageView *image = [self getImageAtPoint:startPoint]; if(image) { [self flickAnimateImage:image toPoint:[self predictedLastPoint]]; } break; default: } } return;

Sensing

As you can see from the example code, creating custom gesture recognisers is a fairly straightforward process, and using them is even easier. You should always consider whether your touch handling needs could be better addressed through a gesture, and then use the gesture if possible. You should also take a few moments to look through the documentation at the supplied GRs, as they may well solve your problems with very little effort required.

Nicolaas tenBroek (AMDG)

405

iOS Development

Sensing

Accelerometer
The accelerometer is one of two devices that provide information about a devices change in attitude in space. The other device is the gyroscope, which we will discuss in the next section. Older iOS devices provide only an accelerometer, while newer ones (i.e. iPhone 4 and later devices) provide both sensors. The reason for the dual sensor approach is that while both measure change in attitude, they are actually measuring different aspects of that change and will each be useful in different scenarios. The accelerometer actually measures the pull of gravity whereas the gyroscope measures the rotational force around an axis. If a device were in free-fall (heaven forbid that it has been dropped, let us instead assume that it may be on a space station) the accelerometer would not measure any change because the force of gravity would be negated but the gyroscope would still be able to measure rotation. If a device were moving along an axis but not rotating (assuming we are back on the planet), the gyroscope would see no change, but the accelerometer would measure the force being applied. Therefore the appropriateness of a given sensor depends entirely on the specific problem you are trying to solve. Some solutions will even require both to be used at the same time. There are two methods for accessing the accelerometer data. The older approach is to deal with the sensor directly, while the newer approach uses a library called Core Motion. We will demonstrate each approach in turn to help illuminate the differences. Regardless of the approach used however, you must always begin your quest for sensor data by figuring out how often your app will need to be updated with new data from the sensor. The accelerometer can provide updates as frequently as 100 times per second (i.e. 100Hz), or as infrequently as you need. There is a cost associated with using the accelerometer, and that cost is assessed through power consumption. In fact, the accelerometer is normally turned off to conserve power, and only activated when an app requests it. Additionally, the higher the frequency of updates, the more power the accelerometer will consume. So this problem is not simply one of accuracy, it is instead one of balancing the need for accuracy against the need for the device to continue running (i.e. to not run out of power). Some problems will have very clearly defined update requirements in their specifications, while others may need to be adjusted by what feels right to the users. To assist with this Apple recommends that you begin with the following frequency ranges and adjust from there as appropriate. Updates in the range of 70 to 100 Hertz should be used for apps that need near real-time information. (This is far from real-time of course, but it is as close as we can get on these devices.) Games and simulation apps work well between 30 and 60 Hertz, while apps that are not time-critical can easily work in the 10 to 20 Hertz range. Acceleration is handled through two very small classes: UIAccelerometer and UIAcceleration, and an equally small protocol: UIAccelerometerDelegate. The UIAccelerometer class packages information about the sensor itself and is very simple. In fact, this class contains only one method and two properties. As there is only one accelerometer sensor on the device, all apps use the same instance of UIAccelerometer to access the sensor. You should never create a new instance of UIAccelerometer, instead access the one method in the class: sharedAccelerometer which gives you access to the single instance. The properties are updateInterval and delegate. While the delegate is perhaps obvious, the updateInterval needs a brief explanation. The choice of the word interval is telling, as it is measured in fractions of a second and is type NSTimeInterval (which you likely recall is a double). Most update

Nicolaas tenBroek (AMDG)

406

iOS Development

Sensing

specifications will list the requirement in Hertz rather than fractions of a second, which makes the use of this property a bit of an annoyance. Thankfully it is quite easy to change from one to the other. Simply divide 1.0 by your Hz requirement, and you will have the appropriate NSTimeInterval value. The accelerometer begins updating as soon as the delegate property has been set, so the proper approach is to first set the updateInterval, and then set the delegate. The class UIAcceleration packages the information about a single measurement of the accelerometers state. It provides four pieces of information: changes along the x, y, and z axis plus a timestamp of the sample. The axis information is provided in units of gravitational force (g-force) where the value 1.0 represents +1g. The UIAccelerometerDelegate protocol provides a single method (accelerometer:didAccelerate:) which is called with new acceleration instances as often as requested by the update interval. Using the accelerometer is thus quite simple. You merely need to implement the delegate, then access the shared accelerometer instance and set both the update interval and delegate properties. There is one more very important piece of information though, and that is stopping the accelerometer updates. Remember that using the accelerometer drains the battery a great deal. Therefore you must remember to disable it when you no longer need accelerometer data. The process for stopping the updates is a simple as starting them. You merely need to set the delegate property to nil, and the accelerometer will disable itself. Depending on your app, you may need the accelerometer running all the time, or only while a certain view is displayed. If it needs to be running all the time, you should start the accelerometer from within the App Delegate. If it needs to be running only for a specific view, then start it within the view. Regardless of the approach you take, you must always remember to stop the accelerometers updates from within the App Delegate so that it is not running while the app is in the background state. After you have the accelerometer running, you need to begin handling all the data it will send your way, which will be quite a lot. Unfortunately, the raw data provided by acceleration events is a bit problematic. The accelerometer is affected by fluctuations in gravity and by constant small environmental changes like vibrations from a passing train, or even building air handling systems. In fact, you would be hard-pressed to find some surface around you that was not vibrating to some extent, and of course, the human body simply cannot hold perfectly still. Those constant movements mean that the accelerometer will constantly record changes, and many of those changes should definitely not affect the outcome of your app. The best way to deal with the raw accelerometer data is to filter it and prevent changes outside of the expected norm from affecting the positional information within your app. There are two types of filters that are typically used with this kind of data: high-pass filters and low-pass filters. Low-pass filters are designed to attenuate the effects of large spikes, allowing lower-frequency changes through the filter whereas high-pass filters are designed to attenuate the effects of lower-frequency changes and allow higher-frequency data through the filter. The needs of the app will drive the decision about the kind of filter you choose. If you are looking for smooth changes in the attitude of the device, then you will need

Nicolaas tenBroek (AMDG)

407

iOS Development

Sensing

a low-pass filter. A high-pass filter will be useful if you are looking for very fast and large attitudinal changes and need to reduce the effect of small changes. Formulas for high and low-pass filters abound on the web, and you are likely to find one that has been designed specifically to fit the needs of the app you are designing. The XCode documentation provides a very simple generic formula to help you get started (and we will use it in our example), but you should spend some time researching the available options to ensure you use one that is appropriate for your app. As a demonstration of this basic approach to using the accelerometer, we will develop a Single View Application that allows you to modify the speed of updates in real-time, and displays the results of both high-pass and low-pass filters and their differences from the raw data. You will need to run this example on an actual device as the simulator cannot generate accelerometer data. We will need a large number of labels for this as we will have X, Y, and Z axis data for the raw updates, low-pass filtered, and high-pass filters. We will also use a slider to change the update interval. In developing the example we used this layout:

Setup your slider in Interface Builder so that the value ranges from 0.0 to 100.0, which will then mirror the update frequencies supported by the accelerometer. We set ours to start the app with a value 50.0 (which will be interpreted as 50Hz by the app). Rather than listing the entire App Delegate implementation here we are listing only the method changes needed. Remember that the accelerometer must be activated only when you actually need it, and must be deactivated whenever you no longer need updates. The timing of those activations and deactivations will vary by app. If your app has multiple screens and does not need the accelerometer on all those screens, then you should activate and deactivate it within the Controller. Regardless of when and where you activate it, you should always deactivate the accelerator in the App Delegate when the app enters the background state. Thankfully, deactivating the accelerometer is easily accomplished. Simply set the delegate property of the shared accelerometer to nil.

Nicolaas tenBroek (AMDG)

408

iOS Development

Sensing

When the application enters the active state, you should examine the current view to determine whether it needs the accelerometer and reactivate it as necessary. Our app has only one view, so we do not need that check, we can safely reactivate the accelerometer in applicationDidBecomeActive:. AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application { [UIAccelerometer sharedAccelerometer].delegate = nil; } - (void)applicationDidBecomeActive:(UIApplication *)application { [self.viewController beginAccelerometerUpdates]; } - (void)applicationWillTerminate:(UIApplication *)application { [UIAccelerometer sharedAccelerometer].delegate = nil; }

Next we will examine our Controller, beginning as always with the header file. We will require IBOutlet properties for the slider and for each label as well as the method we called from the App Delegate for starting the accelerometer and an action method for responding to the slider. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UIAccelerometerDelegate> @property @property @property @property @property @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet UISlider *slider; UILabel *rawXLabel; UILabel *rawYLabel; UILabel *rawZLabel; UILabel *lowPassXLabel; UILabel *lowPassYLabel; UILabel *lowPassZLabel; UILabel *highPassXLabel; UILabel *highPassYLabel; UILabel *highPassZLabel;

- (void)beginAccelerometerUpdates; - (IBAction)sliderValueChanged; @end

In the implementation we begin with a symbolic constant for the accelerometer update frequency. Of course, in our demonstration app we will be using the slider to modify the frequency, but this is not the normal state of affairs. Typically a symbolic constant would be used which is why we provided a

Nicolaas tenBroek (AMDG)

409

iOS Development

Sensing

commented out version. This example is set to update at 60Hz. Remember that using a symbolic constant will reduce your maintenance efforts in future updates. The next symbolic constant is the filtering factor, which sets the limit on the data for the low-pass filter. Our low pass filter will use 10 per cent of the new data and 90 per cent of the accumulated data to produce values that change more smoothly over time. Next we have some pre-processor macros that implement our filters. The use of macros is important here for a couple of reasons. First, the accelerometer updates will be very fast, and for each update we will need to filter three pieces of data. If we stored our filter formula in a method (i.e. the traditional approach), we would be incurring the overhead of three additional method calls as many as 100 times per second. The pre-processor macros effectively provide in-line methods. They look like method calls, but the code does not have the overhead of a method call. The second reason for using a macro is that it prevents us from rewriting the same line of code. We could have simply reproduced the formula each time we needed it, but that approach is error prone. If we ever decided to change the formula used we would need to track down every instance of it and modify it in-place, hoping not to make a mistake. The macro expands in-line, but is only written once, so we have only one change to make when it is necessary. The first low-pass filter macro is using the formula suggested by Apple, whereas the second one (which is commented out) is simply another one for comparison purposes. Again, you must remember to research a filter that best fits your needs. The high-pass filter is also the one suggested by Apple and merely subtracts out the low-pass data. Filtering the data requires that the values be stored in instance variables so that they can accumulate over time. We will need an X, Y, and Z variable each for the low and high-pass filters. We use the type UIAccelerationValue, which is a type definition of a double. ViewController.m (section 1 of 4)
#import "ViewController.h" //#define UPDATE_FREQUENCY_HZ 1.0 / 60 #define FILTER_FACTOR 0.1 #define LOW_PASS_FILTER(filteredData, newData) ((newData) * FILTER_FACTOR + (filteredData) * (1.0 - FILTER_FACTOR)) //#define LOW_PASS_FILTER(filteredData, newData) ((filteredData) + FILTER_FACTOR * ((newData) - (filteredData))) #define HIGH_PASS_FILTER(filteredData, newData) ((newData) - LOW_PASS_FILTER(filteredData, newData)) @implementation ViewController { @private UIAccelerationValue lowX; UIAccelerationValue lowY; UIAccelerationValue lowZ; UIAccelerationValue highX; UIAccelerationValue highY;

Nicolaas tenBroek (AMDG)

410

iOS Development ViewController.m (section 1 of 4)


} UIAccelerationValue highZ; slider; rawXLabel; rawYLabel; rawZLabel; lowPassXLabel; lowPassYLabel; lowPassZLabel; highPassXLabel; highPassYLabel; highPassZLabel;

Sensing

@synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize

The first method in the next code section sets up the accelerometer and activates it. This is the method we called in the App Delegate when the application enters the active state. We begin by retrieving the shared accelerometer and then set the update interval. In the code below we have commented out the code that would normally be used (i.e. the line that uses the symbolic constant) and replaced it with one that reads the slider. With the update interval set we can start the accelerometer by setting the delegate. In our sliderValueChanged action method we simply take the new value of the slider, convert it from Hertz and then update the accelerometer with the new value. Recall that the update interval property is measured in seconds, but typical specifications are measured in Hertz. We can convert Hertz to seconds by simply dividing 1.0 by the required Hertz. Note: Do not divide the integer 1 by the Hertz value or you risk incurring integer division and the result will be zero ((1 / 60) is not the same as (1.0 / 60) or even (1 / 60.0)). ViewController.m (section 2 of 4)
#pragma mark - Public Methods - (void)beginAccelerometerUpdates { UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer]; // accelerometer.updateInterval = UPDATE_FREQUENCY_HZ; accelerometer.updateInterval = 1.0 / slider.value; accelerometer.delegate = self; } #pragma mark - IBActions - (IBAction)sliderValueChanged { [UIAccelerometer sharedAccelerometer].updateInterval = 1.0 / slider.value; }

Nicolaas tenBroek (AMDG)

411

iOS Development

Sensing

In the next code section we implement the UIAccelerometerDelegate protocol by implementing the method accelerometer:didAccelerate:. We use both our high and low-pass filters to aggregate the accelerometer data, and then display the values in our labels. Notice that for the high and low-pass displays we display both the filtered data and the difference between the filtered data and the raw data. With the app running you will see those differences spike when the device moves and then settle when the device stops moving (or rather, when it comes close to stopping). ViewController.m (section 3 of 4)
#pragma mark - UIAccelerometerDelegate - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { lowX = LOW_PASS_FILTER(lowX, acceleration.x); lowY = LOW_PASS_FILTER(lowY, acceleration.y); lowZ = LOW_PASS_FILTER(lowZ, acceleration.z); highX = HIGH_PASS_FILTER(highX, acceleration.x); highY = HIGH_PASS_FILTER(highY, acceleration.y); highZ = HIGH_PASS_FILTER(highZ, acceleration.z); rawXLabel.text = [NSString stringWithFormat:@"%lf", acceleration.x]; rawYLabel.text = [NSString stringWithFormat:@"%lf", acceleration.y]; rawZLabel.text = [NSString stringWithFormat:@"%lf", acceleration.z]; lowPassXLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowX, (acceleration.x - lowX)]; lowPassYLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowY, (acceleration.y - lowY)]; lowPassZLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowZ, (acceleration.z - lowZ)]; highPassXLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highX, (acceleration.x - highX)]; highPassYLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highY, (acceleration.y - highY)]; highPassZLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highZ, (acceleration.z - highZ)]; }

The last section of code contains the View lifecycle methods. We have commented out a version of viewDidAppear: and viewWillDisappear: as a reminder that in a multi-screened app you will need to start and stop the accelerator when these two events occur. ViewController.m (section 4 of 4)
#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; slider.value = 60.0;

Nicolaas tenBroek (AMDG)

412

iOS Development ViewController.m (section 4 of 4)


} /* - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self beginAccelerometerUpdates]; } - (void)viewWillDisappear:(BOOL)animated { [UIAccelerometer sharedAccelerometer].delegate = nil; [super viewWillDisappear:animated]; } */ @end

Sensing

With that small amount of code in place, we can run our app on a device, and test out the effects of differing update intervals and the different signal filters we have applied. Here is a sample of the app in action, showing the dramatic differences between the high and low-pass filters:

Nicolaas tenBroek (AMDG)

413

iOS Development

Sensing

Core Motion
The Core Motion library is designed to give access to attitudinal and motion data in a much more organised and coherent fashion than simply adding libraries for each new sensor that comes along. You can use the library to get raw unfiltered and unbiased data from both the accelerometer and gyroscope just as we did by directly accessing the accelerometer, or you can use Core Motion to access filtered (i.e. gravity removed) and biased data that is combined to provide an extremely accurate relative attitude of the device. Whichever approach you decide to use, Core Motion provides a consistent API, which will make your job as a programmer a bit easier. In addition to a consistent API, Core Motion offers two data delivery mechanisms. In the previous example we set the update interval and all the data was pushed down to our app at that rate. Core Motion provides this same ability, but also adds a polling option (or pull option depending on your linguistic bent) which is both much easier to implement in game and simulation type apps and more efficient from a coding perspective. Regardless of the sensor combination you desire to use, the kind of data you need, or the delivery mechanism chose, all Core Motion programming begins with the class CMMotionManager. The CMMotionManager pulls together all of the (well, as of this writing both of the) sensors, configuring them and starting and stopping them as necessary. This class becomes your main interface to the library and the remaining classes are essentially supporting classes. Core Motion accelerometer data is handled through the class CMAcclerometerData and gyroscope data is handled through the class CMGyroData. When the data from the two sensors are combined they are titled Device Motion and handled through the class CMDeviceMotion. Each of the data classes packages the same kind of data you would get from the sensor itself, but adds a timestamp which is often useful when using the pull approach to data gathering. CMMotionManager is a well-designed class that uses the same pattern of methods for each of the three types of data available. The process begins as it did before by setting the update interval through the appropriate xxxUpdateInterval property (replacing xxx with accelerometer, gyro, or deviceMotion as appropriate) and then using either startXXXUpdates to implement the pull approach or startXXXUpdatesToQueue:withHandler: to implement the push approach. When the sensor data is no longer needed, calling the method stopXXXUpdates will disable the sensor. When using Core Motion it is extremely important that your app create only one instance of CMMotionManager for use in the entire app. If you plan to access motion information in multiple places, then you absolutely must place the CMMotionManager instance in a location where all the other classes can access it. The App Delegate might be a good choice for that location, or if that will not work in your situation, then design a custom singleton class to contain it. If multiple instances of CMMotionManager exist the sensor data will be polled in an inconsistent manner and may well produce invalid readings. Now that we have an idea of how Core Motion works it would be helpful to compare using it to the approach we used earlier, in which we directly accessed the accelerometer. Remember that Core

Nicolaas tenBroek (AMDG)

414

iOS Development

Sensing

Motion provides two completely different mechanisms for providing the data (push and pull), and only the push mechanism can be directly compared to the direct access approach. We will therefore rewrite the previous app to use the push mechanism from Core Motion, which will not require too many changes. The push mechanism is handled with the method:
- (void)startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue withHandler:(CMAccelerometerHandler)handler

NSOperationQueues are mechanisms for organising tasks that typically need to be run repeatedly and often provide a unique thread specifically for executing those tasks. Core Motion allows you to specify the queue to use, so you can choose from the available queues (as we will do in our example) or create a new one if your app requires it. The handler argument is actually a code block with no return value and two arguments. The first argument is the appropriate kind of data object (in this case CMAccelerometerData) and the second argument is an NSError instance. When an error is received you should always stop the sensor updates and then inform the user of the problem. Stopping the sensor first is crucial or the errors will continue to occur and messages will begin to pile up faster than the user can respond. The Core Motion libraries are housed in the Core Motion Framework, which is not added to projects by default. So, just as we did in the chapter on animation, we must add the framework before we do anything else. To access the frameworks we need to select the project icon in the project explorer, and then choose the target. Click on the Build Phases tab and expand the section titled Link Binary With Libraries. Finally, click the + button and choose the Core Motion Framework from the list. We will begin our example by modifying the Controllers header file where we need to add an endAccelerometerUpdates method signature to match the begin method we used before. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <UIAccelerometerDelegate> @property @property @property @property @property @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet UISlider *slider; UILabel *rawXLabel; UILabel *rawYLabel; UILabel *rawZLabel; UILabel *lowPassXLabel; UILabel *lowPassYLabel; UILabel *lowPassZLabel; UILabel *highPassXLabel; UILabel *highPassYLabel; UILabel *highPassZLabel;

Nicolaas tenBroek (AMDG)

415

iOS Development ViewController.h


- (void)beginAccelerometerUpdates; - (void)endAccelerometerUpdates; - (IBAction)sliderValueChanged; @end

Sensing

The first change in the implementation is to import the Core Motion header files. These are only available after the framework has been added to the project, so if you experience an error check that the framework was imported correctly. Using Core Motion does not obviate the need for filters unless you use the Device Motion data. As we are using the accelerometer in this example we will need to create both high and low-pass filters just as we did in the previous example. We will also need a new instance variable to hold the CMMotionManager object. ViewController.m (section 1 of 3)
#import "ViewController.h" #import <CoreMotion/CoreMotion.h> //#define UPDATE_FREQUENCY_HZ 1.0 / 60 #define FILTER_FACTOR 0.1 #define LOW_PASS_FILTER(filteredData, newData) ((newData) * FILTER_FACTOR + (filteredData) * (1.0 - FILTER_FACTOR)) //#define LOW_PASS_FILTER(filteredData, newData) ((filteredData) + FILTER_FACTOR * ((newData) - (filteredData))) #define HIGH_PASS_FILTER(filteredData, newData) ((newData) - LOW_PASS_FILTER(filteredData, newData)) @implementation ViewController { @private CMMotionManager *cmMotionManager; UIAccelerationValue lowX; UIAccelerationValue lowY; UIAccelerationValue lowZ; UIAccelerationValue highX; UIAccelerationValue highY; UIAccelerationValue highZ; } @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize slider; rawXLabel; rawYLabel; rawZLabel; lowPassXLabel; lowPassYLabel; lowPassZLabel;

Nicolaas tenBroek (AMDG)

416

iOS Development ViewController.m (section 1 of 3)


@synthesize highPassXLabel; @synthesize highPassYLabel; @synthesize highPassZLabel;

Sensing

At this point the process will diverge a bit from the one we developed in our earlier example. The method beginAccelerometerUpdates is public, which means we need to be very careful about ensuring it does not run if the updates are currently in process. We can determine that easily enough by simply checking for the presence of a CMMotionManager instance. Remember that each app should only create a single instance of CMMotionManager, so if an instance already exists, we simply return from the method. After we have determined that it is safe to proceed we create the instance of CMMotionManager and then setup the update interval. This is exactly the same interval we used when accessing the accelerometer and is measure the same way, converting Hertz to seconds by dividing 1.0 by the Hertz value. Just as we did in the previous example, we have included commented code to demonstrate the normal method for setting this property using the symbolic constant UPDATE_FREQUENCY_HZ. With the update interval set we can then ask the accelerometer to begin updating using the specified operation queue, and to push the updates to the specified CMAcclerometerHandler block. All of the Core Motion handler blocks take an error argument in addition to the data argument. When an error is present you are required by the API to inform the user and stop sensor updates, which we do straight away at the top of the block. When no error exists we simply apply our filters and update the screen using the exact same methodology we had in our previous example. Next we have the new endAccelerometerUpdates method, which stops the accelerometer and then disposes of the CMMotionManager instance. With the current instance released, it will be safe to create a new one if the updates need to start again. ViewController.m (section 2 of 3)
#pragma mark - Public Methods - (void)beginAccelerometerUpdates { if(cmMotionManager) { return; } cmMotionManager = [[CMMotionManager alloc] init]; // cmMotionManager.accelerometerUpdateInterval = UPDATE_FREQUENCY_HZ; cmMotionManager.accelerometerUpdateInterval = 1.0 / slider.value; [cmMotionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) { if(error) { [self endAccelerometerUpdates]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Accelerometer Error!" message:[error localizedDescription] delegate:nil

Nicolaas tenBroek (AMDG)

417

iOS Development ViewController.m (section 2 of 3)


[alert show]; return;

Sensing

cancelButtonTitle:@"OK" otherButtonTitles:nil];

lowX = LOW_PASS_FILTER(lowX, accelerometerData.acceleration.x); lowY = LOW_PASS_FILTER(lowY, accelerometerData.acceleration.y); lowZ = LOW_PASS_FILTER(lowZ, accelerometerData.acceleration.z); highX = HIGH_PASS_FILTER(highX, accelerometerData.acceleration.x); highY = HIGH_PASS_FILTER(highY, accelerometerData.acceleration.y); highZ = HIGH_PASS_FILTER(highZ, accelerometerData.acceleration.z); rawXLabel.text = [NSString stringWithFormat:@"%lf", accelerometerData.acceleration.x]; rawYLabel.text = [NSString stringWithFormat:@"%lf", accelerometerData.acceleration.y]; rawZLabel.text = [NSString stringWithFormat:@"%lf", accelerometerData.acceleration.z]; lowPassXLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowX, (accelerometerData.acceleration.x - lowX)]; lowPassYLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowY, (accelerometerData.acceleration.y - lowY)]; lowPassZLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", lowZ, (accelerometerData.acceleration.z - lowZ)]; highPassXLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highX, (accelerometerData.acceleration.x - highX)]; highPassYLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highY, (accelerometerData.acceleration.y - highY)]; highPassZLabel.text = [NSString stringWithFormat:@"%lf | %0.2lf", highZ, (accelerometerData.acceleration.z - highZ)]; }]; } - (void)endAccelerometerUpdates { [cmMotionManager stopAccelerometerUpdates]; cmMotionManager = nil; } #pragma mark - IBActions - (IBAction)sliderValueChanged { cmMotionManager.accelerometerUpdateInterval = 1.0 / slider.value; }

Most of the code in the following section is exactly the same as the previous example. Just as before we included commented out versions of viewDidAppear: and viewWillDisappear: to demonstrate how those methods should be used in a multi-view environment, though these have been updated to call the new ending method as appropriate.

Nicolaas tenBroek (AMDG)

418

iOS Development ViewController.m (section 3 of 3)


#pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; slider.value = 60.0; } /* - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self beginAccelerometerUpdates]; } - (void)viewWillDisappear:(BOOL)animated { [self endAccellerometerUpdates]; [super viewWillDisappear:animated]; } */ @end

Sensing

The last step is to update the app delegate and change the way it stopped the accelerometer. Simply replace the call deleting the delegate with a call to our new endAccelerometerUpdates method in the Controller, and our app will be ready to run. AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application { [self.viewController endAccelerometerUpdates]; } - (void)applicationDidBecomeActive:(UIApplication *)application { [self.viewController beginAccelerometerUpdates]; } - (void)applicationWillTerminate:(UIApplication *)application { [self.viewController endAccelerometerUpdates]; }

With the app complete it will run and appear exactly as it had in the previous example. Remember that the only real change here is how we accessed the accelerometer; the physical sensor and the data it provides are exactly the same. In the next example we will demonstrate the pull or polling approach to acquiring sensor data, which is actually a bit simpler than the push method as it requires less code to interact with the CMMotionManager. The pull approach is typically used in games or simulation apps as they already

Nicolaas tenBroek (AMDG)

419

iOS Development

Sensing

contain an externally driven loop that updates on a regular basis. In addition to demonstrating the pull approach we will use the Device Motion information that combines the gyroscope and accelerometer data and pre-filters gravity from the data helping to stabilise the readings. Use of the gyroscope (and by extension, Device Motion) obviously requires a device that contains a gyroscope. At the time of this writing the only devices which do are the iPhones 4 and later, iPod Touch 4th generation and later, and the iPad 2. When choosing to use the gyroscope or Device Motion for motion updates you are forced into a followup decision as well. You can either choose to support only those devices that possess a gyroscope, or write code that handles both classes of devices. The proper decision is entirely based on the app that you are currently developing, and a proper decision for one app may well be improper for another. Obviously you will need to make that decision based on the needs of the app you are currently developing. The gyroscope provides very different motion data than the accelerometer and the combination of the two sensors produces a third qualitatively different kind of data. An app that works beautifully with Device Motion may seem clunky and poorly designed if forced to rely on the accelerator alone. Real-world testing is the only way to be sure if Device Motion code can be ported to support older hardware without damaging the user experience. In the next example we will provide support for both classes of devices to provide an example of detecting the available hardware. If you are lucky enough to have access to multiple devices, you can compare the running code side-by-side, and the differences will be immediately apparent. The polling approach to sensor information is most often used in game-type applications, but those also happen to be very large undertakings. To provide a sample we will create a simple game-style app, but not use any of the advanced graphics features. XCode does include an OpenGL Game Application template, but it provides far more than we need right now. So, we will begin as we often have with a Single View Application and make some changes from there. We will use the same process that games use to update their state and draw the screen, but we will only use that process to update the state of our ball. You will also need to provide an image for this project. We used an image of a ball, but you may use anything you like with two restrictions. First, the image should be fairly small. A ball with a radius of 25 pixels is plenty large, so we do not need a massive or high quality image. Nor does the image have to match the balls size as it will be automatically resized as needed. Second, the image should be in the png format. Other image formats can be handled, but png files are the native type for iOS and will be the easiest to deal with. With the selected image added to the project we are nearly ready to proceed with coding. Before we can start to produce code we must add in the Core Motion Framework, just as we did in the previous example. With those two tasks complete, open the view in IB and add an image view for your ball. Set the images size to 50x50 pixels via the Size Inspector, and centre the image on the view. If the ball image does not have a transparent background, you should change the views background colour to match.

Nicolaas tenBroek (AMDG)

420

iOS Development

Sensing

In the header file for our controller we will need to add an IBOutlet for our ball (dont forget to wire this up!) and two methods: startAnimation and stopAnimation. Those methods are exposed here so that the App Delegate can start and stop the animation in response to the app being started and suspended. We will need several more methods, but they will be handled privately. ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (nonatomic, weak) IBOutlet UIImageView *ball; - (void)startAnimation; - (void)stopAnimation; @end

The implementation begins with a private category to house the methods we will use for starting and stopping the motion sensors as well as some methods for moving the ball about the screen. Next we declare a handful of instance variables for controlling the animation. We have a Boolean called animating which we will use to ensure we do not accidentally create multiple motion managers. This variable was not strictly necessary, but will make our code more legible. The CADisplayLink object helps synchronize drawing with screen refreshes. Games use this object for updating their state and then drawing the scene. We will use it to drive the polling of the sensor data. Next we have an NSTimeInterval (which is a type-defined double) called lastMotionTimestamp that represents a piece of data you must store when using the pull technique with Core Motion. We have not seen this before because the push technique of motion sensing sends your app data only when a new sensor reading is available. The pull technique has no such guarantee. You may well request additional sensor readings before a new reading has happened. In order to determine whether or not the data you received is from a new reading, you must know when the sensor reading happened. Thus, each Core Motion data class extends the class CMLogItem, which provides a timestamp. You will need to compare the timestamp against the last one you received and determine whether or not the data is new. Finally we include a radius which we will use to simplify our motion calculations later. The first method on the agenda is initWithNibName:bundle: and its code is fairly straightforward. We begin by setting animating to false, and nulling the display link reference. These are not strictly necessary steps, but we will be making decisions based on the values in these variables, so it is best to be sure when starting. Next we create an instance of the motion manager and then ask it whether or not device motion is available by checking the property deviceMotionAvailable. If the device supports device motion we set the update interval to 100Hz. If device motion is not supported we instead set the

Nicolaas tenBroek (AMDG)

421

iOS Development

Sensing

accelerometer update interval to 100Hz. The choice of 100Hz probably seems like overkill here, but it is actually the recommended setting for apps employing polling. Remember that polling is typically used in gaming scenarios and speed is the rule in games, not battery life. In addition, the polling will be driven by a separate thread and updating frequency that is essentially out of our control and cannot therefore be completely synchronised with screen updates. Setting the fastest update frequency will help ensure that the majority of our polls return new readings, which will go a long way toward reducing any stuttering in the game. The next change we made to the template is in the viewDidLoad method where we calculate the radius of the ball from the size it was given in IB. Next we add a line to viewDidUnload in which we ensured that the motion updates were no longer running. In truth, the updates should have been long since stopped, but it never hurts to add some extra protection for the users battery. The last change in this section of code was to the shouldAutorotateToInterfaceOrientation: method in which we simply disable all rotation. We need the screen to remain stable so that the ball can move within it, and having a constantly rotating screen will making moving the ball quite a challenge. ViewController.m (section 1 of 4)
#import "ViewController.h" #import <CoreMotion/CoreMotion.h> #import <QuartzCore/QuartzCore.h> @interface ViewController () - (void)startMotionUpdates; - (void)stopMotionUpdates; - (void)moveWithMotion:(CMDeviceMotion *)deviceMotion; - (void)moveWithAcceleration:(CMAccelerometerData *)accelerometerData; - (CGPoint)validateLocation:(CGPoint)location; @end @implementation ViewController { @private BOOL animating; CADisplayLink *displayLink; CMMotionManager *motionManager; NSTimeInterval lastMotionTimestamp; float radius; } @synthesize ball; #pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if(self = [super initWithNibName:nibName bundle:bundle]) { animating = NO; displayLink = nil; motionManager = [[CMMotionManager alloc] init];

Nicolaas tenBroek (AMDG)

422

iOS Development ViewController.m (section 1 of 4)


if(motionManager.deviceMotionAvailable) { motionManager.deviceMotionUpdateInterval = 1.0 / 100; } else { motionManager.accelerometerUpdateInterval = 1.0 / 100; } } return self; } - (void)viewDidLoad { [super viewDidLoad]; radius = self.ball.bounds.size.width / 2; } - (void)viewDidUnload { [super viewDidUnload]; //this should already have stopped, but we want to ensure it has //leaving it running would be disastrous for the user's battery [self stopMotionUpdates]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return NO; }

Sensing

The next section of code contains the methods for starting and stopping motion updates. Both methods employ the same check of the deviceMotionAvailable property which we used in the init method and then use the results of that to start or stop the appropriate sensors. There isnt much to these methods, but they will be called often enough to warrant separating the code. ViewController.m (section 2 of 4)
#pragma mark - Motion Manager - (void)startMotionUpdates { if(motionManager.deviceMotionAvailable) { [motionManager startDeviceMotionUpdates]; } else { [motionManager startAccelerometerUpdates]; } } - (void)stopMotionUpdates { if(motionManager.deviceMotionAvailable) { [motionManager stopDeviceMotionUpdates]; } else { [motionManager stopAccelerometerUpdates]; } }

Nicolaas tenBroek (AMDG)

423

iOS Development

Sensing

The following section is a bit more interesting than the last. The startAnimation method begins by obtaining a reference to the display link and setting up a method to be called (drawFrame). Next we set the frame interval factor to one. This ensures that each time the screen is refreshed the drawFrame method will be called. You can slow down the drawing (and therefore sensor polling) by increasing this factor. For instance, a factor of two would incur call to drawFrame for every two screen refreshes. Finally we add the display link to the main run loop and start the motion sensors. The method stopAnimation is fairly self-explanatory, and drawFrame is nearly as simple. It simply checks to see which sensors we are using, asks the relevant sensor for data, and passes that data to a method which will move the ball. ViewController.m (section 3 of 4)
#pragma mark - Animation - (void)startAnimation { if (!animating) { animating = YES; displayLink = [[UIScreen mainScreen] displayLinkWithTarget:self selector:@selector(drawFrame)]; [displayLink setFrameInterval:1]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [self startMotionUpdates]; } }

- (void)stopAnimation { if (animating) { animating = NO; [displayLink invalidate]; displayLink = nil; } } - (void)drawFrame { if(motionManager.deviceMotionAvailable) { [self moveWithMotion:motionManager.deviceMotion]; } else { [self moveWithAcceleration:motionManager.accelerometerData]; } } [self stopMotionUpdates];

The next section of the controller code contains our motion handling. As we mentioned a bit earlier, this example will demonstrate the coding technique for handling devices that do not support a gyroscope, so we have two Core Motion update methods: one which receives CMDeviceMotion data

Nicolaas tenBroek (AMDG)

424

iOS Development

Sensing

and a second which receives CMAccelerometerData. You will note that the two methods are nearly identical. This is not the proper approach, rather we wrote the code this way so that you could more easily compare the data from the two approaches. The Device Motion data is gravity filtered and needs no modification. The Accelerometer data needs a filter before it can be accurately used for most apps. Generally you should avoid using the raw data directly (which is exactly what we did here). With that caveat in mind, we can take a closer look at what is happening. Both methods begin by checking the sensor readings against the last timestamp. If the timestamps match, then we know this is an old reading and does not need to be processed. Otherwise we record the time of the new reading, and then use the data to move the ball. We decided to multiply the sensor readings by half the radius to get the ball moving about the screen a bit faster. There was not scientific rationale for using half the radius as the modifier, rather it was simply the first modification we tried, and it seemed to be a fair enough speed. Notice that the Device Motion data items are called roll, pitch, and yaw. This change in terminology indicates the combined nature of the data and informs us that this is not a simple change of direction along an axis. After calculating the new location but before actually moving the ball we call the validateLocation method to ensure that the ball remains on the screen. Without this call the ball would move about in an unrestrained space, and disappear for large stretches of time. ViewController.m (section 4 of 4)
#pragma mark - Motion Updates - (void)moveWithMotion:(CMDeviceMotion *)deviceMotion { //ensure this is a new reading if(lastMotionTimestamp != deviceMotion.timestamp) { //save for next check lastMotionTimestamp = deviceMotion.timestamp; CGPoint location = ball.center; //move based on device action location.x += deviceMotion.attitude.roll * radius / 2; location.y += deviceMotion.attitude.pitch * radius / 2; } } - (void)moveWithAcceleration:(CMAccelerometerData *)accelerometerData { //ensure this is a new reading if(lastMotionTimestamp != accelerometerData.timestamp) { //save for next check lastMotionTimestamp = accelerometerData.timestamp; CGPoint location = ball.center; ball.center = [self validateLocation:location];

Nicolaas tenBroek (AMDG)

425

iOS Development ViewController.m (section 4 of 4)


//move based on device action location.x += accelerometerData.acceleration.x * radius / 2; location.y += accelerometerData.acceleration.y * radius / 2; } } - (CGPoint)validateLocation:(CGPoint)location { //ensure we do not leave the viewable area if(location.x - radius <= 0) { location.x = radius; } else if(location.x + radius > self.view.bounds.size.width) { location.x = self.view.bounds.size.width - radius; } if(location.y - radius <= 0) { location.y = radius; } else if(location.y + radius > self.view.bounds.size.height) { location.y = self.view.bounds.size.height - radius; } return location; } @end ball.center = [self validateLocation:location];

Sensing

With the controller complete we need to make some very small changes to the App Delegate. To keep things simple we will only list the changed methods below.

Nicolaas tenBroek (AMDG)

426

iOS Development

Sensing

These updates are far more important than their size would lead you to believe. In applicationDidEnterBackground: and applicationWillTerminate: we ask the controller to stop animating. Remember that this step will turn off the motion sensors, and skipping this step will cause the battery to continue to drain even while our app is suspended. The importance of this step cannot be overstated. Do not forget it! Finally, in applicationDidBecomeActive: we start the animation. AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application { [self.viewController stopAnimation]; } - (void)applicationDidBecomeActive:(UIApplication *)application { [self.viewController startAnimation]; } - (void)applicationWillTerminate:(UIApplication *)application { [self.viewController stopAnimation]; } @end

With this class created we now have a working Core Motion app which polls the sensor data rather than having the data pushed to the app. While we needed some extra code to setup something to move, the actual sensor handling code is much simpler than it was in the push example.

Nicolaas tenBroek (AMDG)

427

iOS Development

Sensing

Nicolaas tenBroek (AMDG)

428

iOS Development

Device Location

Device Location
It could easily be argued that the ability for a hand-held mobile device to determine its precise location has changed the smartphone app industry more than any other single development. In fact, location sensing and tracking has opened up entirely new ways of operating and sharing information, and is even changing the world of mobile games. For many new apps easy access to this kind of information is not a luxury, but an integral part of their functioning, and quite a few of those apps are changing the way people live by changing what they do throughout every day. The location and map APIs provided by iOS make accessing location information easier than you might expect, and you should always consider whether or not location data could be used to add new functionality to your app. Of course, in addition to the technical matters like battery usage, and signal strength, accessing location data requires that you examine the ethical implications of your app as well. You must never access the users location without their express permission, and your app must very explicitly inform the user about how the location information will be (or will not be) used by the app and by your company. If the data is to be stored or transmitted, you must inform the user of this fact. You must also explain that the data will be encrypted, anonymised if necessary, and will or will not be shared or sold. You must be truthful about such practices. Remember that users are often very willing to share such information, but will revolt if they have been misled. You must be very clear and plain about such usage and not hide your intentions in dense legal documents, and you must always provide the user with the ability to both optout of such sharing and the ability to disable the location services whenever they wish. As if ethics and honesty were not motivating enough on their own, it should be noted that failure to follow ethical practices in handling location data will certainly ruin your reputation and may end up with judicial action. It is quite certain that many apps are currently not following these practices, but there are a large number of security researchers who are investigating apps and companies looking for such abuses. The almost certain discovery of their actions will never turn out well for those companies. So, feel free to use the location and map data, be as creative as possible in the use of such information, but do so ethically and chances are it will work out well for you.

Nicolaas tenBroek (AMDG)

429

iOS Development

Device Location

Core Location
While discovering a devices location is remarkably easy to accomplish by using the Core Location library, the topic itself is actually quite involved. There are a few important issues to consider when adding location sensing to an app and it may well take more time to think through those issues than to actually implement the code once the decision has been made. Although the documentation is fairly complete and accurate, it does not approach the topic in a manner that will assist you in making the decisions necessary for properly implementing location sensing. We will endeavour to quickly discuss the factors and decisions in a way that can help you in a real-life situation here, and later provide a simple example of the library in use rather than a complete and properly designed app. The first factor to consider when using the location services is the same factor you must always consider for a mobile app: battery life. The sensors involved in discovering a devices location can deplete a battery at a truly alarming rate. While we have not conducted scientific trials, our experience has been that the location sensors seem to be able to deplete the battery faster than any other single sensor, which makes employing them for any duration a serious affair indeed. Each choice you make from the remaining factors will need to include a consideration of the power costs. The more accurate and frequent your information becomes, the more power it will cost, and the more hesitant users will be to employ the app with any frequency. Remember that a drained battery means both no app usage, and an unhappy user. The second factor to consider is accuracy. As odd as it may seem, location sensing is not an on or off proposition. It takes much more power to resolve a highly accurate location than it does to resolve a more generalised one. If your app needs to know the devices location within a few metres in order to function, that information will come at a much higher price than discovering which city the device is in. While most programmers would opt for the most accurate information possible simply out of habit, this may not be the best approach when dealing with location. When developing a location-sensing application you should instead ask how general the location could be without disturbing the functionality of the app. For instance, if you are developing an app that displays a weather forecast, knowing the precise location and heading of a device does not provide more usable data than knowing only the geographical region. The extra power it would take to get the more precise information would be entirely wasted in that situation. Of course, asking for a generalised location does not prevent the system from returning a highly accurate position; rather it allows the OS to decide when it can stop trying to further resolve the location. All the location data you receive contains measurements of the accuracy of the data. If the device is in an ideal environment the first measurements returned may well be highly accurate. Additionally you should use this accuracy related information to monitor the environment. For instance, the device may be in a structure that obscures the satellite signal and possibly the cellular radio as well. If you need highly accurate location information and the accuracy values do not change over time, then it would be wise to disable the service, notify the user, and provide a mechanism for the user to re-enable the service when it would be more appropriate to use.

Nicolaas tenBroek (AMDG)

430

iOS Development

Device Location

The third factor to consider is update frequency. This is where the mobile part of the app environment comes into play because the device may well be moving. You will need to decide how far the device can move from its last resolved location before you need to recalculate the position. Obviously a navigation app would need to update quite frequently to be of any use as very small changes in location can be incredibly important, but those same changes would likely be meaningless to a social media or weather app. Some app designers opt for a more manual approach. They discover the users location when the app is launched (or re-launched), and then offer a UI control that will update the location each time it is pressed. Offering the user control over the use of the location sensors also offers them control over their power usage in a very easy to implement manner, but offloading the decision from the design team to the user is not usually a good approach. Obviously these decisions will vary from app to app and situation to situation. It will be well worth the time to think them through before coding begins. With the brief discussion of the factors affecting location-sensing design decisions out of the way, we can now look to the implementation. The main API to the Core Location library is provided through the class CLLocationManager. This class provides four different mechanisms for obtaining location-based information: the Standard Location service, the Significant Change service, the Region Monitoring service, and the Heading service. Of these four the Standard Location service is the one you are most likely to be familiar with as it provides the kinds of data typically associated with navigation apps. This is by far the most accurate and controllable of the services as it can be configured to provide information ranging from the very specific and frequently updated location-based information to the very general three-kilometre estimate which is updated infrequently. The data provided by the Standard Location service includes the devices latitude and longitude as well as an accuracy rating of that measurement, which are expected pieces of information. In addition, this service provides altitude, speed, and the direction of travel, making it very useful for any app needing to track the devices movement. As this service provides the most data, we will demonstrate it in our upcoming example app. The Standard Location service is also quite adaptable in that it can access a variety of sources to obtain its location information. For instance, it can triangulate cell-tower signals, use Wi-Fi data, and use GPS hardware to determine the location of a device. This means it can often locate the device in less-than-ideal situations, though the accuracy may or may not be significantly degraded as it switches from GPS to other sources. The degradation of the accuracy is entirely caused by environmental factors outside of both the user and devices control. For instance, the GPS satellite signal is very weak and can easily be blocked by structures or trees. If the GPS signal is unavailable the system will attempt to determine the location through available Wi-Fi and cell-tower information. If the user is in a rural area, it is quite possible that too few towers will be in range for signal triangulation, leaving your app with nothing more than a general idea of its location (i.e. we know you are somewhere within this this cell towers radius, or worse: this is the state that the ISP has reported as the residence for the Wi-Fi IP address). In contrast, the Significant Change service is built entirely upon the idea of a generalised location. This service is intended to provide the most rudimentary location information and to provide updates only after the device has moved a significant distance. Highly accurate data may be provided by the system if it is available, but apps using this service must not rely on the data being accurate. This kind of data is extremely useful for regional information like weather forecasting, fuel prices, grocery coupons, or

Nicolaas tenBroek (AMDG)

431

iOS Development

Device Location

cinema schedules. This service also has the advantage of being extremely power conscious as it relies on information already obtained from the cellular radio, adding little-to-no additional burden on the power reserves for locating the user. Apps using this service are typically updated only when the device changes its associated cell tower (which is one of the definitions of significant for this service), which means repeated runs of an app on a stationary device can all use the same location readings. If the app has been suspended or stopped after enabling significant change notifications, it will be restarted (or relaunched if necessary) and allowed to run in the background for a short period of time to process the region change information. The benefits of this service are likely obvious: low and intermittent power requirements and background processing while still providing fairly regular updates about a devices location. The Region Monitoring service is a very different kind of service from the previous two in that it works by notifying an app when the device has entered a pre-determined region. Like the Significant Change service the Region Monitoring can be used even when the associated app is not running. What is truly interesting about this service though is that it provides targeted location information. The app essentially registers with the system for notification of the region events, and the system will deliver those events when they occur. If the app has been suspended or stopped, it will be restarted (or relaunched if necessary) and allowed to run in the background for a short period of time to process the region change information. This could be used to remind users of items on their to do list as they travel close to necessary destinations, or to deliver location-based advertising. For instance, a coffee shop may provide an app that notifies customers of daily specials whenever they are close to the shop. Like the Significant Change service, this approach is a relatively low-power approach to location sensing, but adds immediately useful information as well. The numbers of regions that can be monitored by a single app are limited as all the apps on the device share region resources. If a large number of regions need to be monitored, the app is responsible for registering only those regions that are closest to the user at a given time, and deregistering regions when they are no longer necessary. The final service is the Heading service that provides access to a digital compass for determining a devices heading in relation to both true north and magnetic north. The digital compass works by measuring changes in the Earths magnetic field. Metal objects, building wiring, electric motors, and a host of other everyday type items can easily confound the fields detected by these devices. To combat this problem iOS uses fairly sophisticated algorithms that filter out the extraneous information. However, this filtering often requires the device to move a bit when the service is started in order to calibrate the compass with the current environment. Thankfully, detecting the poor conditions and entering the calibration mode are quite easy to do, and running the calibration is easy for the user as well. We will demonstrate this technique in the forthcoming example app. Regardless of the type of location identification you decide to employ in you app, there are some decisions you as the app designer must handle. For instance, what will your app do if location information is unavailable? Can the app function with little or no location information? Is the specific location critical or can some parts of the app continue to function with a generalised region? How will your app handle the variety of devices on the market, some which do not have GPS receivers and compasses? If the app can function with or without specific location information, then you need only to

Nicolaas tenBroek (AMDG)

432

iOS Development

Device Location

add the logic to your app to handle the situations where it is present and where it is not. If you decide that your app simply cannot function without location information or even more specifically, without a GPS, then you must add entries to the Required device capabilities key in the apps Info.plist file. The following image shows the entries necessary for an app that requires GPS. If a more general location is required, simply eliminate the line indicating the GPS (however, in order to require that the device support GPS both lines must be in the plist).

Keep in mind, these settings in the plist are intended for use only by apps that absolutely require location services and cannot function without them. iTunes will use the plist information to prevent the app from being installed on a device that does not support location services. In fact, if both location services and GPS were indicated as we did in the previous screen shot, iTunes would prevent the app from being installed on a device that did not contain a GPS receiver. If this is truly a requirement of the app and the app cannot function without such a device, then you definitely should add the information to the Required device capabilities in order to prevent a user from installing the app on a device which cannot run it. If the app would simply prefer to have these services but can also work without them, then you must not add the location services values to the plist. In addition to identifying the app requirements, we must also consider user authorisation and servicelevel issues. Each app that attempts to access location-based information must receive direct user approval. If the user denies the request, then the app must deal with the situation appropriately. If there are portions of the app that can function without the location information, then those areas must continue to run even if the app does not receive approval to access the location services. If nothing in the app will work without location information (i.e. as in our upcoming example), then you must inform the user that they need to approve the request before the app will be able to work correctly. The very first time an app starts location services on a device the OS will present a dialogue indicating that the app is requesting access to the users location. At that point the user will have the ability to approve or deny the request. On subsequent runs, it becomes the apps responsibility to notify the user if it does not have permission, but the app cannot request access directly again. The user can enable or disable location permissions on an app-by-app basis at any time via the Settings app under the Location Services menu. Therefore, if your app does not receive permission and the user attempts to use parts of the app that require location services to work, the app should direct the user to authorise your app via the Settings app. Unfortunately at the time of this writing it is impossible to automatically switch to

Nicolaas tenBroek (AMDG)

433

iOS Development

Device Location

the Settings app from within your app, which means directing the user is all you can do. Given that the location services are a bit of a mystery to some people, it may be a wise idea to add a tutorial to your products website to assist those users who do not fully understand how to configure and control their device. In addition to direct user authorisation, the app must detect whether or not the location services have been disabled for the entire device (which would mean your app has permission to access the sensors, but they are unavailable). There are two mechanisms for disabling location services for the entire device. First, the user may have enabled Airplane Mode in the Settings. This mode turns off all radios including cellular, Wi-Fi, and GPS, which will make it impossible to determine the location. Alternatively the user can simply disable all location services (also via Settings). Regardless of the reason for location services being disabled, the rules for handling the situation are the same as the rules for not receiving authorisation: the non-location-based parts of the app should continue to function, and the rest should inform the user that location services are not available, and how that situation may be remedied. Now that we have a good overview of Core Location and how it functions, we can implement a small example. This example will simply demonstrate the retrieval of some of the location-based information, and is not what one might refer to as practical (though we prefer to call it focused or perhaps demonstrative). In this example we will use two location services: the standard location service and the heading service. We will demonstrate the proper techniques for accessing both services, and will simply display the data on the screen. Begin by creating a Single View Application. To continue our established tradition of rather uncreative app names, we decided to name our app Location, though you may of course call yours whatever you wish. Before we can get into the code we need to add the Core Location library to the project, so you should do that now. The screen will be fairly busy in this app as we have a great deal of data to display and a few functional controls to allow us to investigate some of the settings while the app is operational. Here is a screenshot of the setup we used:

Nicolaas tenBroek (AMDG)

434

iOS Development

Device Location

We will deal with actual data and interpretation a bit later, but at this stage we can set the limits of the three sliders. The first slider is labelled Accuracy Level and ranges from a minimum value of 0.00 to a maximum value of 5.00. Notice that we have set the Current Value to 1.00. This is actually an important setting, so be sure to set yours the same way. Next we have the slider labelled Distance Filter. This sliders values range from -1.00 to 100.00 and is currently set to -1.00. Finally, we have the slider labelled Heading Filter which ranges from -1.00 to 359.99 and is currently set to -1.00. You likely noticed we also have two images. These are simply little pointer-type images that we created for this app, and will serve to indicate the direction of the compass. Any pointy image will work well, though obviously it cannot be overly large as we do not have much space for it. Our images are 40x40 pixels square, and your image should be square as well. We will be rotating the images in code so that they always point north, and a non-square image may interfere with other controls as it rotates. With the screen design complete we can turn our attention to the interesting bits in the code. We will begin in the Controllers header file where we need to indicate that we will implement the CLLocationManagerDelegate protocol. The methods in this protocol will be used to notify the app when new sensor readings become available. We will next need a fair number of IBOutlets for displaying the large amount of data on the screen. After creating these, be sure to go back into Interface Builder and wire-up the connections. Take a close look at the IBAction methods. You will note that we have the three sliderChanged type methods which will be attached to the Value Changed actions you would expect to have when using sliders, but we also have a fourth method which we called accuracyTouchesEnded. We provided this extra method because we need the accuracy slider to work with integer values, and the UISlider is designed to support only double values. To ensure selections always end on an integer value and more importantly, to ensure that the user is not terribly confused when this happens, we will need to handle

Nicolaas tenBroek (AMDG)

435

iOS Development

Device Location

some extra events. You will need to wire-up this single method to both the Touch Up Inside and the Touch Up Outside actions of the accuracy slider. Finally we provide the signature for two public methods: beginLocationUpdates and endLocationUpdates. We exposed these methods so that they could be called from the app delegate. Remember that as with any other sensor, we are responsible for turning the location sensors on and off when our app is started and stopped. Failure to turn off the sensors will quickly drain the devices batter, and make our users quite unhappy. ViewController.h
#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> @interface ViewController : UIViewController <CLLocationManagerDelegate> @property @property @property @property @property @property @property @property @property @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet UIImageView *magneticArrowImage; UIImageView *trueArrowImage; UILabel *magneticHeadingLabel; UILabel *trueHeadingLabel; UILabel *latitudeLabel; UILabel *longitudeLabel; UILabel *altitudeLabel; UILabel *speedLabel; UILabel *accuracyLevelLabel; UILabel *distanceFilterLabel; UILabel *headingFilterLabel; UISlider *accuracySlider; UISlider *distanceFilterSlider; UISlider *headingFilterSlider;

(IBAction)accuracySliderChanged; (IBAction)accuracyTouchesEnded; (IBAction)distanceFilterSliderChanged; (IBAction)headingFilterSliderChanged;

- (void)beginLocationUpdates; - (void)endLocationUpdates; @end

This app will require a few instance variables to store the small amount of data that we will need access to. Notice that we have two arrays, one called accuracyLevels and the other called accuracyStrings. Core Location supports 6 levels of accuracy, and those are represented by the constants kCLLocationAccuracyBestForNavigation, kCLLocationAccuracyBest, kCLLocationAccuracyNearestTenMeters, kCLLocationAccuracyHundredMeters, kCLLocationAccuracyKilometer, and kCLLocationAccuracyThreeKilometers. Typically these kinds of values would be stored in an enumeration, but for some reason they are all stand-alone constants. This

Nicolaas tenBroek (AMDG)

436

iOS Development

Device Location

design decision may be related to the fact that apps typically operate with one or two settings, and do not let the user make adjustments. Of course, those kinds of adjustments are exactly what we are going to provide to the user in our example. So, to make it easier to choose from the constants we will store the constant values in an array, and use a second array to store string representations for display on the screen. In the first section of our implementation we have a bit of code that you likely did not expect to see. The method batteryStateChanged: is to be called whenever the battery changes state. The possible battery states are: unknown, unplugged, charging, and full, though we are not examining the actual state within this method. We need to monitor the batterys state because Apple recommends that the highest level of accuracy (kCLLocationAccuracyBestForNavigation) only be used when the device is plugged in. There are a very few exceptions to this rule (i.e. exercise-tracking apps rarely operate in an environment that permits the device to be plugged in, but still need high-accuracy). That high level of accuracy uses the radios quite extensively which will very quickly deplete a battery and therefore should be typically used only when the device is plugged in. In this example we will demonstrate the proper technique for tracking the batterys state and ensuring the high-accuracy operates only when the device is attached to an external power source. The method batteryStateChanged: is a key component of that monitoring, as it will be called whenever the batterys state changes (which you may have guessed given the name of the method). We are not terribly interested in the actual state of the battery at this point because in later sections of the code we will ensure that we only enter the high-accuracy state when the device is plugged-in. Therefore, when the method is called we simply check the current accuracy level. If the battery state changes and the level is currently set to the highest accuracy (value 0.00 which we have associated with kCLLocationAccuracyBestForNavigation), then we know the device has been unplugged, and we should therefore change to a lower accuracy level (in this case we changed to 1.00 which we have associated with kCLLocationAccuracyBest). Updating the accuracy level is as simple as setting the desiredAccuracy property of the CLLocationManager instance, but in our case we must also update the display to reflect the change. Therefore we update both the slider and the display level to reflect the updated setting. ViewController.m (section 1 of 5)
#import "ViewController.h" @implementation ViewController { @private CLLocationManager *locationManager; BOOL isUpdating; float currentAccuracyValue; NSArray *accuracyStrings; NSArray *accuracyLevels; } @synthesize magneticArrowImage; @synthesize trueArrowImage;

Nicolaas tenBroek (AMDG)

437

iOS Development ViewController.m (section 1 of 5)


@synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize magneticHeadingLabel; trueHeadingLabel; latitudeLabel; longitudeLabel; altitudeLabel; speedLabel; accuracyLevelLabel; distanceFilterLabel; headingFilterLabel; accuracySlider; distanceFilterSlider; headingFilterSlider;

Device Location

#pragma mark - Private Methods - (void)batteryStateChanged:(NSNotification *)notification { if(!currentAccuracyValue) { currentAccuracyValue = 1.0f; accuracyLevelLabel.text = [accuracyStrings objectAtIndex:currentAccuracyValue]; accuracySlider.value = currentAccuracyValue; locationManager.desiredAccuracy = [[accuracyLevels objectAtIndex:currentAccuracyValue] doubleValue]; } }

In the next section of code we will deal with the starting and stopping of the location updates. The two methods presented in this section are the ones that we exposed publicly and in most situations must be called by the app delegate to prevent location sensing in the background. If you are designing a realtime navigation-based app, then you would require a slightly different setup as most real-time navigation apps would need to continue updating the location even if the user is doing something else with the device (i.e. receiving a phone call or looking something up on the web). We start beginLocationUpdates by checking to see if we are already receiving the location updates. This may seem an odd step, but we need to keep in mind that this method is public. As such, it may be called from several different locations within an app, and this check will head off any potential problems. Next we must check to ensure that location services are available, and that we either have permission to access them or that we have not yet asked for such authorisation. As we mentioned earlier, the first time an app is run on a given device the OS will inform the user that the app would like to access the location and offer the user the chance to approve or deny the request. That choice is not actually presented by the OS until the app attempts to access the location information. Therefore, we will treat an authorisation status of authorised and not determined as the same, and attempt to turn on the location services. The other possible values for authorisation status are restricted and denied. The denied status informs us that the user actively denied the apps request for access. The restricted

Nicolaas tenBroek (AMDG)

438

iOS Development

Device Location

status means that the user is being prevented from giving permissions either due to corporate policy or parental control settings. When have determined that we are ready to enable the location services we begin by setting up the desired accuracy and distance filters. The accuracy slider indicates the desired accuracy level and that value is then used as an index into the accuracy levels array we discussed earlier. The distance filter determines how far (in metres) a device will need to move before an update is generated. Setting that value to kCLDistanceFilterNone (or -1 on the slider our app is using) uses the minimum distance the device is able to detect. With those two properties configured we then request that the location manager start updating locations. We will handle the actual location events a bit later. Because our sample app is demonstrating both the location and heading services (supplied by the compass) we next ask the location manager if the heading service is available. Not all iOS devices contain the digital compass, so it is always advisable to check the availability before enabling the service. When enabling the heading service we must also set a filter to control how often our app will receive updates. The heading filter is measured in degrees, and like the distance filter supports a value of kCLHeadingFilterNone (also -1 on our apps slider). The last step for enabling location services is to enable monitoring of the battery state. You are not always required to take this step, but our example app allows the user to set the accuracy of the readings, and you will recall that the highest level of accuracy should only be used when the device is plugged in. Enabling battery state monitoring is a two-step process. First, the app must register itself with the NSNotificationCenter to receive battery state change notifications, and second we must ask the device to begin generating those notifications by setting the batteryMonitoringEnabled property to YES. As odd as it may seem, monitoring the batterys power level will consume the battery power. In fact, most device notifications are disabled by default in order to conserve battery power. This of course means that we must remember to disable the notifications when they are no longer needed. All considered our demonstration app is fairly power-intensive, and we must be vigilant about conserving the power when we can. In the cases where either location services are disabled or the app has been denied access, we will simply present an alert to the user informing them of the problem. Notice that we customised the message based on the actual cause of the failure. This will help guide users who are not familiar with the system to the solution. Unfortunately iOS does not yet provide a publicised mechanism for directly switching to the Settings app. The best we can do at the moment is try and be clear about why the app cannot access the location services. Next, in endLocationUpdates we disable both the heading and location updates and then also disable battery-state monitoring. We actually present two approaches to handling the Notification Centre updates here for comparison purposes. The first approach will remove the current object from all registered updates, and the commented-out line will remove the current object from only the battery state updates.

Nicolaas tenBroek (AMDG)

439

iOS Development ViewController.m (section 2 of 5)


#pragma mark - Public Methods

Device Location

- (void)beginLocationUpdates { if(isUpdating) { return; } if([CLLocationManager locationServicesEnabled] && ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined)) { locationManager.desiredAccuracy = [[accuracyLevels objectAtIndex:currentAccuracyValue] doubleValue]; locationManager.distanceFilter = distanceFilterSlider.value; [locationManager startUpdatingLocation]; if([CLLocationManager headingAvailable]) { locationManager.headingFilter = headingFilterSlider.value; [locationManager startUpdatingHeading]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryStateChanged:) name:@"UIDeviceBatteryStateDidChangeNotification" object:[UIDevice currentDevice]]; [UIDevice currentDevice].batteryMonitoringEnabled = YES; isUpdating = YES; } else { NSString *message = nil; if(![CLLocationManager locationServicesEnabled]) { message = @"This app requires location services to run, but the services are disabled. Please enable those services via the Settings and re-launch this app."; } else { message = [NSString stringWithFormat:@"This app requires location services to run, but is not authorised to access those services. Please authorise the \"%@\" app via the Settings and re-launch it.", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]]; } UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location Services Required" message:message delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }

- (void)endLocationUpdates { [locationManager stopUpdatingLocation]; [locationManager stopUpdatingHeading]; [UIDevice currentDevice].batteryMonitoringEnabled = NO; [[NSNotificationCenter defaultCenter] removeObserver:self]; //NOTE: If this app were set up to receive multiple device notifications,

Nicolaas tenBroek (AMDG)

440

iOS Development ViewController.m (section 2 of 5)

Device Location

the proper line to use for removal of battery state monitoring would be: // [[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIDeviceBatteryStateDidChangeNotification" object:[UIDevice currentDevice]]; isUpdating = NO; }

Next we will examine the CLLocationManagerDelegate protocol implementation. This protocol provides the callback methods for the location and heading updates. Generally, these are fairly easy methods to implement as you simply receive the data and do whatever it is you need to do with them. However, in general use you must first check the timestamp of the update you receive in order to see if it is valid. This is because the location manager will retain its last location reading when it is disabled. Then, when re-enabled it will deliver that last reading right away, which means you could receive quite old data. This happens because resolving a location can take several seconds (which may seem like forever to a user), and the last reading may well be useful if the user has not moved too far since the last reading. You can then check the timestamp of the reading and decided whether or not to use it. If the reading was taken relatively recently, then it is probably a safe starting point, and you can start your processes knowing that a more accurate reading will be delivered shortly. If the data is too old (according to your standard), then you can simply discard the information. In either case you should present some animation or other information to the user so that they know the system is working on resolving their location. We did not take this very important step in our app as it is only designed to be a demonstration vehicle and will not be seen by the public. In addition to checking the timestamps, you should always check the accuracy level of location readings. Any negative accuracy value informs you that the system has not yet received valid data for that item, whereas positive accuracy values tell you how accurate the reading is. In our demonstration app we are displaying the location information and the accuracy level to help you see how the readings are affected by the different settings. In a real app you should find a more graphical mechanism to indicate to the user the accuracy of the information being presented. For instance, the supplied map app shows the users location with a pin, and surrounds the pin with a circle to indicate the accuracy of the location. The only part you might find odd in the update methods shown here would be the rotation code in locationManager:didUpdateHeading:. The heading information is supplied in degrees from both true north and magnetic north. We then use those heading readings with a Core Animation affine transform to rotate the image. The CA transformation requires its rotations to be in radians, so we have to do a quick and easy conversion from degrees to radians. After the update methods we see the method locationManagerShouldDisplayHeadingCalibration: which is called whenever the heading updates are started. If this method returns a YES value, the location manager will automatically display a calibration screen for the compass which will instruct the user to wave the device around in a figure-eight pattern. This pattern will help calibrate the compass by allowing it to more clearly detect the interfering magnetic fields. In our example we return YES only if

Nicolaas tenBroek (AMDG)

441

iOS Development

Device Location

the location manager has taken a compass reading and the accuracy is invalid. This can happen when the app is started and the device is resting on or near a large piece of metal or an interfering device. The code demonstrated here is likely the least conservative approach and may well allow through invalid compass readings. You are free to indicate more frequent calibrations are necessary if your app demands a higher level of accuracy. The last method in this section is locationManager:didFailWithError: and as its name implies is called to inform your app of a failure to retrieve location information. There are any numbers of reasons those failures that could occur and while we did not deal with the specific reasons here, you may need to do so in a real app. This method will be called if you attempt to enable location sensing and the app has been denied permission, or if the device is completely shielded from the appropriate radio data. You will need to decide the proper approach for handling these errors based on the implementation of your specific app. For instance, you can recheck the permissions and ask the location manager if the services are available and then make decisions from there. There are two critical steps that need to be taken when location failures occur, and we handled both in this example. First, you must turn off location updates, and then you must also inform the user of the situation so that they are not waiting for the location to resolve. While we used a pop-up dialogue to inform the user, you are not required to user that mechanism. There are of course many ways the user can be notified, but you need to ensure the notification is obvious. You should also provide a mechanism for re-enabling the location updates if the user can fix the issue (by moving to an open area for instance). We did not provide such a mechanism in the example, but you should not interpret the lack as an indication of its importance in a real app. ViewController.m (section 3 of 5)
#pragma mark - CLLocationManagerDelegate - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { NSString *accuracyString = nil; if(newLocation.horizontalAccuracy >= 0) { accuracyString = [NSString stringWithFormat:@"+/- %0.2lf metres", newLocation.horizontalAccuracy]; } else { accuracyString = @"location is invalid"; } latitudeLabel.text = [NSString stringWithFormat:@"%0.2lf (%@)", newLocation.coordinate.latitude, accuracyString]; longitudeLabel.text = [NSString stringWithFormat:@"%0.2lf (%@)", newLocation.coordinate.longitude, accuracyString]; if(newLocation.verticalAccuracy >= 0) { accuracyString = [NSString stringWithFormat:@"+/- %0.2lf metres", newLocation.verticalAccuracy]; } else { accuracyString = @"altitude is invalid";

Nicolaas tenBroek (AMDG)

442

iOS Development ViewController.m (section 3 of 5)

Device Location

} altitudeLabel.text = [NSString stringWithFormat:@"%0.2lf (%@)", newLocation.altitude, accuracyString]; if(newLocation.speed >= 0) { accuracyString = [NSString stringWithFormat:@"%0.2lf mps (%0.2lf kph)", newLocation.speed, (newLocation.speed * 3.6)]; } else { accuracyString = @"speed is invalid"; } speedLabel.text = accuracyString; //note that we could also use newLocation.course to get information about the direction the device is travelling } - (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading { trueArrowImage.transform = CGAffineTransformRotate(CGAffineTransformIdentity, newHeading.trueHeading * -M_PI / 180); magneticArrowImage.transform = CGAffineTransformRotate(CGAffineTransformIdentity, newHeading.magneticHeading * -M_PI / 180); trueHeadingLabel.text = [NSString stringWithFormat:@"%0.2lf", newHeading.trueHeading]; magneticHeadingLabel.text = [NSString stringWithFormat:@"%0.2lf", newHeading.magneticHeading]; } - (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager { CLHeading *heading = manager.heading; return (heading && (heading.headingAccuracy < 0)); } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location Failure!" message:[error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; [self endLocationUpdates]; }

The next section of code handles the updates from the sliders. While the filter sliders simply use their values to update the appropriate heading and distance filters, the accuracy slider is quite a bit more complicated. Recall that the location manager supports six accuracy settings, and they are each represented by a constant value. We created an array to hold the constants (called accuracyLevels), and a second array to hold strings describing the constants (called accuracyStrings).

Nicolaas tenBroek (AMDG)

443

iOS Development

Device Location

We will use the value from the accuracy slider as an index into the arrays, but in order to do that we need to convert it from a double to an integer value. The UISlider does not support a discrete mode which would restrict the input to whole values, which means it will always allow the user to select any point along the slider. While it would be very simple to cast the sliders double value to an integer, doing that would create a rather confusing interface for the user. The interaction effect of using only a cast would be that as the sliders thumb was moved short distances the app would appear to not notice the change, and the user would wonder if it were functional at all. They might then move the thumb a large distance, and then see a change that would give the appearance of the slider working intermittently. Obviously that intermittent and buggy appearance is the antithesis of the impression we wish to impart. To give our slider the appearance of supporting only discrete values and also responding to all input we need two action methods: accuracySliderChanged and accuracyTouchesEnded. Recall that in Interface Builder we wired up accuracySliderChanged to the valueChanged action of the slider and accuracyTouchesEnded to both the touchUpInside and touchUpOutside actions. Additionally we enabled constant update events so that the accuracySliderChanged method will be called each time the sliders thumb is moved regardless of whether or not the user has let it go. This constant update will help the user see the results of their changes before they have actually committed to the selection (which they do by lifting their finger). The code in accuracySliderChanged is actually quite simple. We begin by rounding the sliders value using the round method. Casting a real number to an integer has the effect of always rounding down, whereas the round method will round down if the fractional portion is below one half, and up otherwise. We then check to see if the new value is the same as the old stored value, if it is, then we have no work to do and return. While not strictly a necessary check, this step will keep the interface from being updated unnecessarily and will therefore avoid bogging down the UI. If the new value is different, we store the new value and update the display with the new accuracy level description. Note that we do not yet change the accuracy of our location sensing. We will make that change only after the user stops modifying the slider. In accuracyTouchesEnded we have a bit more to do than simply changing the accuracy setting of the location manager. Before we can make any changes we need to verify that the user has not selected the best for navigation accuracy when the device is unplugged. If the user made such a selection we inform them of the problem, and then reset the accuracy value and display to best. With the level selection verified we then update the slider itself to reflect the integer value we will use. This reset of the value will move the thumb on the display. So, when the user releases the thumb it will snap to the closest whole value location on the slider, but will not change the value being selected. This action will very quickly inform the user that the slider supports only discrete changes and they will more fully comprehend the proper use of this slider. With the sliders value reset we finally modify the location managers accuracy level, which may have an immediate effect on the location information (if we moved from a very general requirement to a very specific one).

Nicolaas tenBroek (AMDG)

444

iOS Development ViewController.m (section 4 of 5)


#pragma mark - IBAction methods - (IBAction)accuracySliderChanged { float newValue = round(accuracySlider.value); if(newValue == currentAccuracyValue) { return; } currentAccuracyValue = newValue; accuracyLevelLabel.text = [accuracyStrings objectAtIndex:currentAccuracyValue];

Device Location

- (IBAction)accuracyTouchesEnded { if(!currentAccuracyValue) { UIDevice *device = [UIDevice currentDevice]; if(device.batteryState == UIDeviceBatteryStateUnknown || device.batteryState == UIDeviceBatteryStateUnplugged) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Unable To Set Accuracy" message:@"That accuracy level is only available when the device is plugged in." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; currentAccuracyValue = 1.0f; accuracyLevelLabel.text = [accuracyStrings objectAtIndex:currentAccuracyValue];

} } accuracySlider.value = currentAccuracyValue; locationManager.desiredAccuracy = [[accuracyLevels objectAtIndex:currentAccuracyValue] doubleValue];

- (IBAction)distanceFilterSliderChanged { distanceFilterLabel.text = [NSString stringWithFormat:@"%0.2f", distanceFilterSlider.value]; locationManager.distanceFilter = distanceFilterSlider.value; } - (IBAction)headingFilterSliderChanged { headingFilterLabel.text = [NSString stringWithFormat:@"%0.2f", headingFilterSlider.value]; locationManager.headingFilter = headingFilterSlider.value; }

In the penultimate section of code for the Controller we handle the standard view lifecycle methods, and while not complicated, they do deserve a quick look. In the initWithCoder: method we create the CLLocationManager instance that is used throughout the class and configure a few important properties. The delegate is important for obvious reasons, and without it our app will not receive updates. The headingOrientation might seem a bit odd at first though. Here we set that to a device face up orientation, which tells the manager, we intend to hold the device flat with the face up. The location

Nicolaas tenBroek (AMDG)

445

iOS Development

Device Location

manager needs this information to know how we want it to interpret the magnetic fields. While we did not handle it in this example, you could easily receive the device orientation change notifications and then update the location manager each time the orientation changed for more accurate data. The next property we set is purpose and is used when the location manager first requests permission to enable location sensing for an app. This string should describe why your app is requesting access to the location information and how it will be used. If you are planning international versions of your app then this string should also be localised. Obviously the purpose property must be set before the location sensing is enabled in order for the location manager to use it in the request for permission. The last step in init is to set up the arrays used for the accuracy settings. The contents of these arrays will not change, so we used the non-mutable NSArray to store the data. In viewDidLoad we setup the labels that describe the sliders value settings. We easily could have handled the setup of these labels in Interface Builder, but by setting them here we ensure that they properly match the sliders values. By changing the display here we ensure any future change to the starting values of the sliders in IB will always be handled properly. Finally, in the viewDidAppear: and viewWillDisappear: methods we enable and disable the location updates as appropriate. ViewController.m (section 5 of 5)
#pragma mark - View lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if((self = [super initWithNibName:nibName bundle:bundle])) { locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; locationManager.headingOrientation = CLDeviceOrientationFaceUp; locationManager.purpose = @"Demonstration of location sensing tools."; accuracyStrings = [[NSArray alloc] initWithObjects:@"Best For Navigation", @"Best", @"Nearest Ten Metres", @"Hundred Metres", @"Kilomretre", @"Three Kilometres", nil]; accuracyLevels = [[NSArray alloc] initWithObjects: [NSNumber numberWithDouble:kCLLocationAccuracyBestForNavigation], [NSNumber numberWithDouble:kCLLocationAccuracyBest], [NSNumber numberWithDouble:kCLLocationAccuracyNearestTenMeters], [NSNumber numberWithDouble:kCLLocationAccuracyHundredMeters], [NSNumber numberWithDouble:kCLLocationAccuracyKilometer], [NSNumber numberWithDouble:kCLLocationAccuracyThreeKilometers], nil]; } return self; } - (void)viewDidLoad { [super viewDidLoad]; currentAccuracyValue = round(accuracySlider.value); accuracyLevelLabel.text = [accuracyStrings objectAtIndex:currentAccuracyValue];

Nicolaas tenBroek (AMDG)

446

iOS Development ViewController.m (section 5 of 5)

Device Location

distanceFilterLabel.text = [NSString stringWithFormat:@"%0.2f", distanceFilterSlider.value]; headingFilterLabel.text = [NSString stringWithFormat:@"%0.2f", headingFilterSlider.value]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self beginLocationUpdates]; } - (void)viewWillDisappear:(BOOL)animated { [self endLocationUpdates]; [super viewWillDisappear:animated]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // Return YES for supported orientations return (interfaceOrientation == UIInterfaceOrientationPortrait); } @end

The last step is to modify the app delegate. We need to ensure that location updates are disabled when the app enters the background, and are then restarted when the app is restored. As we discussed earlier, this step is critical for most apps, but may not be correct for a real-time navigation-type app. Use your best judgement when dealing with the app delegate, but do not forget to address it. AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application { [self.viewController endLocationUpdates]; } - (void)applicationWillEnterForeground:(UIApplication *)application { [self.viewController beginLocationUpdates]; }

Nicolaas tenBroek (AMDG)

447

iOS Development

Device Location

With the code in place, we can now run our little location demo. This app will work best outside away from trees and tall buildings, and frankly, if you are also moving. If you would like to see highly accurate data then you should test the app on a device that contains a GPS receiver. Following are three screenshots from our test run (well, test drive really because we simply cannot run that fast). This first image demonstrates the automated process of the system requesting permission before an app can first access location information:

The next shots demonstrate the app running:

Nicolaas tenBroek (AMDG)

448

iOS Development

Device Location

Maps
Maps on iOS are a bit odd when compared to the other libraries. Most of the libraries are completely self-contained, fully-functional units as presented in the API. In contrast, the Map Kit API provides only the basic framework for supporting the maps while the maps themselves and much if not all of the of the data needed for making them useful comes from outside the device. Specifically that data comes from Google at the time of this writing, though there are rumours of Apple providing internal map support in the near future. The main focus of Map Kit is the MKMapView, which, as its name implies, is a subclass of UIView and provides a UI widget for displaying a map and handling user interaction with the map. The map can display a street view that resembles the typical navigational map, a satellite view that uses satellite imagery of the terrain, or a hybrid view that blends the other two. User interaction supports standard pinch-zoom gestures for changing the zoom level of the map, swipe gestures for scrolling, and taps for selecting specific items. In addition to the map itself, MKMapView provides support for both annotations and overlays, which are useful for adding customised meaningful information to the display. Annotations are generally used to represent specific point-based information like a users location or destination, where overlays are most often used to callout a region of the map like a city, a campus, or other general area. Annotations are represented on the map with images and therefore can be meaningfully selected to convey information about the annotation visually. In contrast overlays are shapes drawn on top of the map and are intended to highlight a region or section of the map. Overlays can be any shape and use the standard CG drawing mechanisms that are typically used in the drawRect: method of a view (though overlays use the method drawMapRect:zoomScale:inContext:). Map Kit provides two convenience mechanisms for accessing location-based data, but both are quite simple. The first provides for automated tracking and display of the devices current location. This mechanism is easy to use and imparts a professional look and feel with almost no effort. The second mechanism is called a Geocoder and can convert coordinates to address-type information (Reverse Geocoding) or take address information and convert that to a map coordinate with both latitude and longitude (Forward Geocoding). In iOS version 5 Apple introduced a geocoding system using the class CLGeocoder which provides both forward and reverse geocoding. The new system can be used with or without a map which allows for a great deal of creative freedom. If you wish to support older versions of iOS you will need to use a reverse geocoder system supplied by Google. The older Google system requires use in conjunction with the Google map which is provided in MKMapView. Any further information needs will have to be supplied from an outside service. For instance, Google provides a service called the Google Maps API Family. Within that family the Google Maps API Web Services provides APIs for directions, geocoding, elevation, and places. While a free service, use of the Google Maps API requires creating an account with Google and comes with its own set of restrictions on use. The use of such services is a bit beyond the scope of this book, so we will instead focus on using the supplied libraries for basic map functionality. Regardless of the geocoding system you decide to use, each app must use the reverse lookups judiciously as the licence agreement provides for a limited number of lookups per app in a given

Nicolaas tenBroek (AMDG)

449

iOS Development

Device Location

timeframe. The documentation for CLGeocoder and MKReverseGeocoder provide more information about how the lookups should be handled in the relevant service. Our example will use the new CLGeocoder, though it is not much different from the MKReverseGocoder. Rather than using the simplistic device location mechanism supplied in Map Kit, our example project will utilise the standard location-sensing mechanisms we covered in the previous section and demonstrate annotation and overlay use, as well as basic reverse geocoding. The example will provide tracking for an entire route as is often found in exercise apps, and provide for saving and retrieving one route with some basic trip summary information thrown in for fun. We will use annotations for the data points along the route and an overlay to indicate the accuracy of the current position (just as many mapping apps do), with the reverse geocoding used to indicate our starting point. Both the annotations and overlays allow for customisation and provide basic versions within the library. We will use supplied versions of both of these as well as customised annotations. Begin by creating a new project from the Utility Application template. We named our project MapDemo, but you of course are free to name yours however you wish. We need both the CoreLocation and MapKit frameworks, so those should be added to the project straight away. We will also need images for the route annotations. The images can be very simple and should be quite small. We used the following three images:

The first (orange) image will be used for the starting point, the second (yellow) will be used to represent the midpoints of the route, and the last (multi-coloured) will be used to indicate the current location.

Nicolaas tenBroek (AMDG)

450

iOS Development

Device Location

The main view will be used to display the map, and the flipside view will display controls and trip summary information. When adding the UIMapView to the view, it will expand to take up the entire view, which is exactly what we want. Unfortunately, in doing so it will cover up the info button which allows us to access the flipside view. You will need to fix this in one of two ways. One way is to delete the info button, then add the map, and then add a new info button (dont forget to wire it up!). The second way is to add the map and then change the order of the controls in the view. Expand the views control listing on the left side of the Interface Builder screen, and simply drag the map up so that it is higher in the list than the info button. This will change the order in which the items are added to the view and ensure that the info button is on top. Here is a screen-shot of what the order should look like:

And here are screen shots of the main view and flipside view designs:

Our first class will be an implementation of the MKAnnotation protocol and is used by the map in conjunction with an Annotation View to display an annotation. We will use the library-supplied class MKAnnotationView to display our annotations, but you may subclass it if you wish to provide additional

Nicolaas tenBroek (AMDG)

451

iOS Development

Device Location

behaviours. MKAnnotationView is itself a subclass of UIView, so you can add virtually any behaviour you wish and the process for doing so is the same as for any view. In addition to the MKAnnotation protocol, our spots will implement the NSCoding protocol, which will allow us to easily save out routes to a file, and then re-load them whenever we wish to relive that trip. As our spots can be in one of three states (starting, mid, and current) we will also need to create an enumeration to indicate the state. This information will then be used to ensure that the view displays the proper image. The annotation is fairly simple, as the MKAnnotation protocol requires only that you provide a property for the coordinate of the annotation, and optionally a title and subtitle to display when the user taps on the annotation view. We will add to those pieces the state of the spot and the timestamp when the location reading took place. Annotation views are reused on maps in much the same way that tables reuse cells, and for the same reason. As an annotation view is scrolled off the screen, it becomes available for re-use and can then display an additional annotation without requiring additional resources. In fact, the process is implemented in a near identical manner to the tables process even to the point of requiring each annotation view be identified by a reuse identifier string. In order to simplify the map code later, we will provide a method in the annotation that returns an appropriate reuse identifier given the state of a spot. Finally, we also add a method for retrieving a new annotation view appropriately initialised for displaying the current spot. SpotAnnotation.h
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> #import <CoreLocation/CoreLocation.h> typedef enum { STARTING_SPOT, MID_SPOT, CURRENT_SPOT } SpotState; @interface SpotAnnotation : NSObject <MKAnnotation, NSCoding> @property (nonatomic, assign) CLLocationCoordinate2D coordinate; @property (nonatomic, readonly) NSDate *spotDate; - (id)initWithState:(SpotState)spotState coordinate:(CLLocationCoordinate2D)spotCoordinate andDate:(NSDate *)date; - (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate withDate:(NSDate *)date; - (NSString *)reuseIdentifier; - (MKAnnotationView *)spotAnnotationView; @end

Nicolaas tenBroek (AMDG)

452

iOS Development

Device Location

We begin the implementation of our SpotAnnotation with the NSCoding protocol methods. To keep things simple we opted for a Keyed Archive approach just as we have used several times before. In fact, there is nothing out of the ordinary in this section as SpotAnnotation is a quite simple data storage class. SpotAnnotation.m (section 1 of 5)
#import "SpotAnnotation.h" @implementation SpotAnnotation { @private NSDate *spotDate; SpotState state; } static static static static NSString NSString NSString NSString *const *const *const *const DATE_KEY = @"SPOT_DATE"; STATE_KEY = @"SPOT_STATE"; LAT_KEY = @"SPOT_LAT"; LONG_KEY = @"SPOT_LONG";

@synthesize coordinate; @synthesize spotDate; #pragma mark - NSCoding - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:spotDate forKey:DATE_KEY]; [encoder encodeInt:state forKey:STATE_KEY]; [encoder encodeDouble:coordinate.latitude forKey:LAT_KEY]; [encoder encodeDouble:coordinate.longitude forKey:LONG_KEY]; } - (id)initWithCoder:(NSCoder *)decoder { if((self = [super init])) { spotDate = [decoder decodeObjectForKey:DATE_KEY]; state = [decoder decodeIntForKey:STATE_KEY]; double latitude = [decoder decodeDoubleForKey:LAT_KEY]; double longitude = [decoder decodeDoubleForKey:LONG_KEY]; coordinate = CLLocationCoordinate2DMake(latitude, longitude); } return self; }

Nicolaas tenBroek (AMDG)

453

iOS Development

Device Location

In section two of the implementation we see the init method and a custom setter for the coordinate property, which is part of the MKAnnotation protocol. We decided to use a custom setter because each annotation stores the date at which the coordinate was obtained. In the absence of any additional data we will use the current system date as the reading date. We also add a second setter, which allows us to supply the coordinate and date. SpotAnnotation.m (section 2 of 5)
#pragma mark - (id)initWithState:(SpotState)spotState coordinate:(CLLocationCoordinate2D)spotCoordinate andDate:(NSDate *)date { if((self = [super init])) { state = spotState; coordinate = spotCoordinate; spotDate = date; } return self; } - (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate { [self setCoordinate:newCoordinate withDate:[NSDate date]]; } - (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate withDate:(NSDate *)date { coordinate = newCoordinate; spotDate = date; }

In section three of the annotations implementation things are a bit more interesting. Here we take the annotations state into account to provide varying behaviour implementations when creating the annotation title. The title method is also part of the MKAnnotation protocol and is used when a user selects an annotation view on the screen; it is typically displayed in a small popup above the annotation. Due to the infrequent nature of this display, you can safely calculate the string at the moment it is requested without incurring a performance hit (assuming your method completes quickly enough of course). In fact, it would be a waste of memory to create these strings when the object is created because the vast majority of them will simply never be seen. In the title method we use the spots state to determine the exact string to display, and also the amount of information that should be displayed. For the starting spot we use the short date and time styles to format the timestamp, while mid spots and current spots will display only the time. There are two items worth additional note here. First, we covered all possible cases in the switch statement, and so included a default case that logs an error. This is good practice and should always be followed. This default case will only appear if the state is improperly set at some other location, or if additional states are added to the enumeration and the person who makes that change forgets to

Nicolaas tenBroek (AMDG)

454

iOS Development

Device Location

account for them here. Note that because this is an error condition we log out the offending state value. That information will be extremely helpful to the person who has to deal with the error. The second item of note is that we used French-braces to create code blocks around our case code. This is highly unusual for any C-derived language and was done as a workaround for an oddity in the Objective-C language. Specifically, Objective-C seems unable to process a variable declaration as the first line of a cases code block. There are three mechanisms you can choose from to solve this systemspecific problem. First, you can declare your variables before the switch block. Obviously if you are going to use the variables after the switch has ended this would be the only viable approach and completely proper. In any other case this early declaration can serve as a breeding ground for bugs when the system is updated at a later date, and should be avoided at all costs. The second possible solution is to insert an empty statement (i.e. a semi-colon) at the beginning of each case, and the third is to use French-braces as we have done. From an implementation standpoint there is no difference between the last two, so you (and your team) should simply decide which one you will use and be consistent about it. Whichever solution you use, it would be wise to document why you are using it to help those who will be maintaining the codebase. SpotAnnotation.m (section 3 of 5)
- (NSString *)title { switch(state) { case STARTING_SPOT: { //code block to deal with Objective-C's problem with creating variables as the first line in a switch NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [NSLocale systemLocale]; [formatter setTimeStyle:NSDateFormatterShortStyle]; [formatter setDateStyle:NSDateFormatterShortStyle]; NSString *formattedDate = [formatter stringFromDate:spotDate]; return [NSString stringWithFormat:@"Started: %@", formattedDate]; } case MID_SPOT: { //code block to deal with Objective-C's problem with creating variables as the first line in a switch NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [NSLocale systemLocale]; [formatter setTimeStyle:NSDateFormatterShortStyle]; [formatter setDateStyle:NSDateFormatterNoStyle]; NSString *dateString = [formatter stringFromDate:spotDate]; return dateString; } case CURRENT_SPOT: { //code block to deal with Objective-C's problem with creating variables as the first line in a switch NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [NSLocale systemLocale]; [formatter setTimeStyle:NSDateFormatterShortStyle]; [formatter setDateStyle:NSDateFormatterNoStyle];

Nicolaas tenBroek (AMDG)

455

iOS Development SpotAnnotation.m (section 3 of 5)


NSString *dateString = [formatter stringFromDate:spotDate];

Device Location

return [NSString stringWithFormat:@"Current Location. Arrived at: %@", dateString]; } default: } } NSLog(@"UNKNOWN SPOT STATE WHEN CREATING TITLE!! %d", state); return [NSString string];

The penultimate section of code contains the method reuseIdentifier, which simply returns a string which the map will use when dequeuing a view for reuse. Note that as we have done in the past the reuse identifiers are declared as static so that we continue to use the same strings and avoid creating new (and unnecessary) ones. Just as we did in the title method, we log an error when we encounter an unknown spot state. SpotAnnotation.m (section 4 of 5)
- (NSString *)reuseIdentifier { static NSString *reuseIdentifierStart = @"STARTING_SPOT"; static NSString *reuseIdentifierMid = @"MID_SPOT"; static NSString *reuseIdentifierCurrent = @"CURRENT_SPOT"; switch(state) { case STARTING_SPOT: return reuseIdentifierStart; case MID_SPOT: return reuseIdentifierMid; case CURRENT_SPOT: return reuseIdentifierCurrent; default: NSLog(@"UNKNOWN SPOT STATE WHEN CREATING REUSE IDENTIFIER!! %d", } } return [NSString string];

state);

Finally we list the spotAnnotationView method, which creates the new view for display based on the state of the spot. We set the annotation views size to a twenty by twenty pixel square simply because that looked appropriate enough. The zoom level of the map does not determine the size of the annotation view, so the relative size of the spot on the map will be entirely determined by the zoom level of the map. In this method we yet again demonstrate proper use of the default case when all cases have been accounted for in the switch. This is an easy to overlook practice, but will save you hours of debugging time should an error occur.

Nicolaas tenBroek (AMDG)

456

iOS Development SpotAnnotation.m (section 5 of 5)


- (MKAnnotationView *)spotAnnotationView { static NSString *imageNameStart = @"orange_spot.png"; static NSString *imageNameMid = @"yellow_spot.png"; static NSString *imageNameCurrent = @"multi_spot.png";

Device Location

NSString *imageName = nil; switch(state) { case STARTING_SPOT: imageName = imageNameStart; break; case MID_SPOT: imageName = imageNameMid; break; case CURRENT_SPOT: imageName = imageNameCurrent; break; default: NSLog(@"UNKNOWN SPOT STATE WHEN CREATING ANNOTATION VIEW!! %d", state); } MKAnnotationView *annotationView = [[MKAnnotationView alloc] initWithAnnotation:self reuseIdentifier:[self reuseIdentifier]]; annotationView.image = [UIImage imageNamed:imageName]; annotationView.canShowCallout = YES; CGRect rect = annotationView.frame; rect.size.width = 20; rect.size.height = 20; annotationView.frame = rect; return annotationView; } @end

As you would expect, the MainViewController is the hub of this demonstration app, but because we are drawing a variety of services together, it may be busier than you had suspected. We are implementing no less than three declared protocols and one implicit protocol (for handling changes to the power state), making this class a very busy hub indeed. To those protocols we add one action method for displaying the flipside view (from the code template), and two public methods for starting and stopping the location updates. (Do not forget to wire-up both the IBOutlet and the MKMapViewDelegate, or things will be quite boring when the app runs.) MainViewController.h
#import "FlipsideViewController.h" #import <MapKit/MapKit.h> #import <CoreLocation/CoreLocation.h>

Nicolaas tenBroek (AMDG)

457

iOS Development MainViewController.h


#import "SpotAnnotation.h"

Device Location

@interface MainViewController : UIViewController <FlipsideViewControllerDelegate, CLLocationManagerDelegate, MKMapViewDelegate> @property (nonatomic, weak) IBOutlet MKMapView *mapView; - (IBAction)showInfo:(id)sender; - (void)beginLocationUpdates; - (void)endLocationUpdates; @end

We will begin the implementation of the main view controller by declaring a small private interface containing a few methods. The first method will be used to adjust our location accuracy by the state of the battery and is exactly the same code we saw in the previous location example. The second method is simply a means to remove all of the mark-ups we will add to the map, thereby returning it to its pristine original state. The last two methods support our reverse geocoding efforts. We need a small handful of instance variables to keep track of the state of the app, though nothing out of the ordinary. Following the class setup we partially implement the MapViewDelegate protocol. The protocol contains methods for handling positional and regional changes, the downloading of new map tiles, basic user tracking, sub-view management for both annotations and overlays, and user-interaction with said sub-views. We will not be using all the functionality available through the protocol, as we need only the view producing methods. The first method is mapView:viewForAnnotation: and is called whenever the map decides to display an annotation to the user. Unfortunately, because Objective-C does not support method overloading, we are faced with the same issue here as we dealt with when providing cells for a UITable. Specifically, we have only one method to write regardless of the number of different annotations we will use on our map. If the map is using only one kind of annotation then this will not be a problem, but if we decide to use multiple types we will need to be quite careful about the construction of our logic. In those situations it would be best to ensure that the most used annotations are handled first, and the least used last. Doing so will help the maps performance remain quick for the user. If you are not sure about the proportions within your specific collection of annotations, then collecting some usage data would be well worth the time and effort. The mix in our demonstration app is quite clear, so we will not need to do such data collection. We will support two annotations, one is the Spot that we developed earlier, and the other is a Pin that is provided in the library. We will use a spot for each location reading we receive, and a single pin to indicate the starting location. Regardless of the number of annotation types you will use, the implementation of this method should be quite simple: we need only examine the

Nicolaas tenBroek (AMDG)

458

iOS Development

Device Location

incoming annotation, dequeue a view, reset the views annotation, and return the view. If the map cannot dequeue a view, then we must create a new one and return that. The only other method we will implement from the protocol is mapView:viewForOverlay: which as its name implies, is responsible for providing views for overlays. Generally an overlay is a shape customised to cover a specific location on a map (i.e. a building, park, or other area of interest), and therefore would not need to be reused in the way an annotation view would. Given the unique nature of overlays the map does not provide a means for reusing them, forcing you to either recreate the overlays each time they are displayed or to implement a reuse policy of your own. Our example will use only one overlay: a light-blue circle (with a darker blue outer ring) indicating the devices current location and accuracy of the location information. The circle overlay is supplied with the library and unfortunately does not provide for any kind of reuse, as all of its properties are read-only. Given the number of times this particular overly will be recreated (once for every location update), it would be well worth our time to write a custom and reusable circle overlay were this a real app. MainViewController.m (section 1 of 6)
#import "MainViewController.h" @interface MainViewController () - (void)setAccuracyByBatteryState; - (void)removeMapMarkup; - (void)reverseGeocodeLocation:(CLLocation *)location; - (void)cancelGeocoding; @end @implementation MainViewController { @private CLGeocoder *reverseGeocoder; MKPlacemark *startingPlacemark; MKCircle *currentLocationCircle; NSString *fileName; CLLocationManager *locationManager; NSMutableArray *locationHistory; SpotAnnotation *currentPointAnnotation; double distanceTotal; BOOL isUpdating; BOOL mapRegionSet; } @synthesize mapView; #pragma mark - MapViewDelegate Methods - (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *pinReuseIdentifier = @"PIN_REUSE_IDENTIFIER";

Nicolaas tenBroek (AMDG)

459

iOS Development MainViewController.m (section 1 of 6)

Device Location

if([annotation isKindOfClass:[SpotAnnotation class]]) { MKAnnotationView *spot = [mapView dequeueReusableAnnotationViewWithIdentifier: [((SpotAnnotation *)annotation) reuseIdentifier]]; if(!spot) { spot = [((SpotAnnotation *)annotation) spotAnnotationView]; } else { spot.annotation = annotation; } } return spot;

if([annotation isKindOfClass:[MKPlacemark class]]) { MKPinAnnotationView *pin = (MKPinAnnotationView *) [mapView dequeueReusableAnnotationViewWithIdentifier:pinReuseIdentifier]; if(!pin) { pin = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pinReuseIdentifier]; pin.pinColor = MKPinAnnotationColorGreen; pin.animatesDrop = true; //not truly needed as the pin is often added to the map when the flipside view is displayed pin.canShowCallout = true; } return pin; } } return nil;

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay { if([overlay isKindOfClass:[MKCircle class]]) { MKCircleView *circle = [[MKCircleView alloc] initWithCircle:overlay]; circle.fillColor = [UIColor colorWithRed:0.6f green:0.788f blue:0.94f alpha:0.4f]; circle.strokeColor = [UIColor colorWithRed:0.486f green:0.635f blue:0.76f alpha:0.8f]; return circle; } return nil; }

Next we will also implement just two methods from the CLLocationManagerDelegate protocol. We need only the location updates and the error notifications for this demonstration, so this section will be a bit simpler than it might be in a real app. We begin locationManager:didUpdateToLocation:fromLocation: with a bit of error checking as any reading with a negative horizontal accuracy is inaccurate and should not be used. We also ignore any updates that provide the same location as the previous reading. Remember that in a real app when attempting to simply locate (i.e. not track) the device, if the Nicolaas tenBroek (AMDG) 460

iOS Development

Device Location

subsequent readings are the same you should turn off the location sensors as they are already providing the best available data. As this is a tracking app we will keep the sensors running, but do not need to update the map. The user is likely quite aware that they have not moved and do not need us to inform them of that fact! Frankly, for our app to be more accurate we should have also checked the timestamp of the initial reading and discarded it if too much time has passed. Recall that the first reading returned is often an old one stored in the devices memory and may be wildly inaccurate for the current location. After deciding that we will use the new location reading, we then check to see whether or not the maps region has been set. By default the map first displays then entire globe. Setting the region is how we get the map to display a specific location and to zoom in to the appropriate level. When we determine the region has already been set, we next need to determine if this is the second or a subsequent reading. The second reading will be used to create the current location annotation, which will be marked with the multi-coloured spot. All subsequent readings will simply move the current location annotation to avoid recreating that object. If the current location annotation is to be moved, we must replace it on the map with a mid-point annotation, which we can do by retrieving the mid-point from the locationHistory array. This action will give us the trail of yellow spots marking our path. The current location annotation is removed but will be added again momentarily. Next we create the mid-point annotation that will be displayed after the next update and store it in the locationHistory array. If this step turns out to be the last one in our journey then this last mid-point annotation will simply never be displayed. Next we reposition the map to keep it centred on the current location. This is the only change you should make to the region so that the map will keep the zoom level it is currently set to. We will specifically set the zoom level in a moment, but we do not want to override any user settings, so we will only set the zoom level one time. This way the user can control what they see on the map, which is always a bonus. With the current location setup and the map repositioned, we can turn our attention to the blue circle overlay. Recall that the supplied circle overlays cannot be modified, so we must remove the old one and create a new one. We will use the horizontal accuracy reading as the radius of the circle, so as the readings get more accurate the circle will get smaller. This is a wonderful visual piece of information for the user and is used in several map apps, so it will be one the user both recognises and understands immediately. The last step for a subsequent reading is to update the distance travelled calculations for use on the flipside view. If this turns out to be the first reading we will need to setup the region for the map to display. We chose a very simple region centred on the current location with a radius of one kilometre. As we mentioned, the user can change the zoom level after this and their choice will remain unchanged by our app. With the region correctly displayed, we can turn our attention to the starting location of our journey. This spot will be displayed with the orange graphic and simply needs to be created at the current location. The last step then is to create and start an instance of the reverse geocoder. This will attempt to resolve the address of our current location. When it has been resolved, the information will be made available via the pin annotation, which should drop into our map and land at the centre of the orange spot. We will examine the reverse geocoding methods soon, but before we do you should recall that each app has a limited number of requests it can make of this service in a given timeframe. It would therefore be a very bad idea for you to attempt to lookup the current location each time a location update is received.

Nicolaas tenBroek (AMDG)

461

iOS Development

Device Location

If you want to provide the ability for your users to lookup their current location, then you should tie that lookup to an action. For instance, if the user taps the current location spot, that action could be used to initiate a lookup. If you implement something like that, be sure to inform the user that the lookup is in progress as it may take several seconds depending on the current network conditions. Additionally, the instructions on geocoding insist that if a user makes repeated lookup requests without changing their position significantly you should simply return the same information without performing a new lookup. MainViewController.m (section 2 of 6)
#pragma mark - LocationManagerDelegate Methods - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { if(newLocation.horizontalAccuracy < 0) { //ignore invalid readings return; } if(newLocation.coordinate.latitude == oldLocation.coordinate.latitude && newLocation.coordinate.longitude == oldLocation.coordinate.longitude) { //if location has not changed, ignore update return; } if(mapRegionSet) { if(currentPointAnnotation) { //remove current point & replace with 'mid point' [mapView removeAnnotation:currentPointAnnotation]; [mapView addAnnotation:[locationHistory objectAtIndex:[locationHistory count] - 1]]; //move current point to new location [currentPointAnnotation setCoordinate:newLocation.coordinate withDate:newLocation.timestamp]; } else { //create current point currentPointAnnotation = [[SpotAnnotation alloc] initWithState:CURRENT_SPOT coordinate:newLocation.coordinate andDate:newLocation.timestamp];

//create mid point for later use SpotAnnotation *annotation = [[SpotAnnotation alloc] initWithState:MID_SPOT coordinate:newLocation.coordinate andDate:newLocation.timestamp]; [locationHistory addObject:annotation]; //move to new current point [mapView addAnnotation:currentPointAnnotation]; [mapView setCenterCoordinate:currentPointAnnotation.coordinate animated:YES];

Nicolaas tenBroek (AMDG)

462

iOS Development MainViewController.m (section 2 of 6)

Device Location

//create highlighting circle with radius sized to reflect accuracy of location reading [mapView removeOverlay:currentLocationCircle]; currentLocationCircle = [MKCircle circleWithCenterCoordinate:newLocation.coordinate radius:newLocation.horizontalAccuracy]; [mapView addOverlay:currentLocationCircle]; //update distance calculation distanceTotal += [newLocation distanceFromLocation:oldLocation]; } else { [mapView setRegion:MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 1000.0, 1000.0) animated:YES]; mapRegionSet = YES; //create starting point SpotAnnotation *annotation = [[SpotAnnotation alloc] initWithState:STARTING_SPOT coordinate:newLocation.coordinate andDate:newLocation.timestamp]; [mapView addAnnotation:annotation]; [locationHistory addObject:annotation]; //get placemark for starting location [self reverseGeocodeLocation:newLocation]; } } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location Failure!" message:[error localizedDescription] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; [self endLocationUpdates]; }

The reverse geocoding process is quite simple. You need only supply the CLGeocoder with a CLLocation object and a code block to call when the process is complete. The code block needs two arguments: an error and an array which will contain CLPacemark objects. When you are reverse geocoding as we are in this example, the array will contain at most one placemark. Forward geocoding takes the same codeblock argument and can produce multiple placemarks if the exact location cannot be determined. If an error occurs and this was a critical or user-initiated lookup, then it would be imperative that you inform the user of the failure. The address information was secondary to our app, so we simply logged out the failure for our own edification and then retired the reverse geocoder. If the address lookup is successful then we can create an MKPlacemark object from the CLPlacemark and add it to the map. MKPlacemark implements the MKAnnotation protocol, so adding it to the map will

Nicolaas tenBroek (AMDG)

463

iOS Development

Device Location

generate a request for a view. Our class will respond to that request by providing an animated green pin, which should drop into place. When the user taps the pin they will see a string representation of the address in the pins callout. The callout space is severely limited, so if you wish to display the entire address, a custom annotation view would be called for, or you could provide a detail disclosure button and display an additional screen with the full information. The cancelGeocoding method is a helper we will use to cancel the geocoding if it is in progress and no longer needed. Keep in mind that most of your testing will probably take place in ideal circumstances (possibly even with WiFi available) and your requests will return quite quickly. Your users will often be in less-than-ideal situations and the request might take some time to resolve. Therefore it is important that you plan to cancel unneeded requests. MainViewController.m (section 3 of 6)
#pragma mark - Geocoding Methods - (void)reverseGeocodeLocation:(CLLocation *)location { reverseGeocoder = [[CLGeocoder alloc] init]; [reverseGeocoder reverseGeocodeLocation:location completionHandler: ^(NSArray *placemarks, NSError *error) { //we are using the geocoder only once, so we can now dispose of it. reverseGeocoder = nil; if(error) { NSLog(@"Reverse Geocoding failed with error:\n%@", error); return; } if(placemarks && [placemarks count]) { startingPlacemark = [[MKPlacemark alloc] initWithPlacemark:[placemarks objectAtIndex:0]]; [mapView addAnnotation:startingPlacemark]; } }];

- (void)cancelGeocoding { if(reverseGeocoder && reverseGeocoder.geocoding) { [reverseGeocoder cancelGeocode]; } reverseGeocoder = nil; }

While the FlipsideDelegate protocol section contains a fair amount of code, most of it is quite simple and should be familiar to you. For instance, the beginLocationUpdates and endLocationUpdates methods are nearly identical to the ones we used in the previous example. In beginLocationUpdates we have added a call to the private method removeMapMarkup which ensures that we begin with a clean map each time. In endLocationUpdates we added a check to deal with the reverse geocoder. If it is

Nicolaas tenBroek (AMDG)

464

iOS Development

Device Location

running when location updates are disabled, then we simply cancel the data request and destroy the object. The method setMapType: allows the user to determine which map to display. By default we display the typical navigational map, but recall that the map can also display a satellite view and a hybrid of the two. The flipside view will contain controls allowing the user to toggle the display. Here we simply pass the users selection on to the map and it handles the rest. The method showInfo: is a bit longer than the template provided, with the additional code calculating the trip summary and setting up the controls to reflect the current state of the app. The last two methods are saveTrip and loadTrip, which are provided to demonstrate how an exercise app might save data about a run and re-display it at a later time. The app provides for a single saved route, but as you can see, it would be trivial to implement this feature for real. In the interest of keeping the app simple we implemented the NSCoding protocol in our annotations and simply save them to a file. A real app would most likely use a database or network-based storage for such data. In loadTrip the entire route is reapplied to the map with every feature except the blue circle overlay so as to indicate that it is not live data. In hindsight it occurs to us that this might have been a perfect opportunity to use another reverse geocoded pin, so that a historical route could display both the start and end points. MainViewController.m (section 4 of 6)
#pragma mark - Flipside Methods - (void)beginLocationUpdates { if(isUpdating) { return; } if([CLLocationManager locationServicesEnabled] && ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined)) { [self removeMapMarkup]; [self setAccuracyByBatteryState]; locationManager.distanceFilter = kCLDistanceFilterNone; [locationManager startUpdatingLocation]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryStateChanged:) name:@"UIDeviceBatteryStateDidChangeNotification" object:[UIDevice currentDevice]]; [UIDevice currentDevice].batteryMonitoringEnabled = YES; isUpdating = YES; } else { NSString *message = nil; if(![CLLocationManager locationServicesEnabled]) {

Nicolaas tenBroek (AMDG)

465

iOS Development MainViewController.m (section 4 of 6)

Device Location

message = @"This app requires location services to run, but the services are disabled. Please enable those services via the Settings and re-launch this app."; } else { message = [NSString stringWithFormat:@"This app requires location services to run, but is not authorised to access those services. Please authorise the \"%@\" app via the Settings and re-launch it.", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"]];

} }

} UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location Services Required" message:message delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show];

- (void)endLocationUpdates { [locationManager stopUpdatingLocation]; [UIDevice currentDevice].batteryMonitoringEnabled = NO; [[NSNotificationCenter defaultCenter] removeObserver:self]; //NOTE: If this app were set up to receive multiple device notifications, the proper line to use for removal of battery state monitoring would be: //[[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIDeviceBatteryStateDidChangeNotification" object:[UIDevice currentDevice]]; [self cancelGeocoding]; isUpdating = NO; } - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller { [self dismissModalViewControllerAnimated:YES]; } - (void)setMapType:(MKMapType)mapType { mapView.mapType = mapType; } - (IBAction)showInfo:(id)sender { FlipsideViewController *controller = [[FlipsideViewController alloc] initWithNibName:@"FlipsideViewController" bundle:nil]; controller.delegate = self; controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; [self presentModalViewController:controller animated:YES]; controller.mapTypeSegmentedControl.selectedSegmentIndex = mapView.mapType; if([locationHistory count]) { SpotAnnotation *lastSpot = [locationHistory objectAtIndex:[locationHistory count] - 1]; SpotAnnotation *firstSpot = [locationHistory objectAtIndex:0]; double tripTime = [lastSpot.spotDate timeIntervalSinceReferenceDate]

Nicolaas tenBroek (AMDG)

466

iOS Development MainViewController.m (section 4 of 6)

Device Location

[firstSpot.spotDate timeIntervalSinceReferenceDate]; controller.averageSpeedLabel.text = [NSString stringWithFormat:@"%0.2lf kph", ((distanceTotal / tripTime) * 3.6)]; controller.distanceTravelledLabel.text = [NSString stringWithFormat:@"%0.2lf Kilometres", (distanceTotal / 1000.0)]; int hours = tripTime / 3600; int minutes = (tripTime - (hours * 3600)) / 60; int seconds = tripTime - (hours * 3600) - (minutes * 60); controller.tripDurationLabel.text = [NSString stringWithFormat:@"%02d:%02d:%02d", hours, minutes, seconds]; } else { controller.averageSpeedLabel.text = @"0.0 kph"; controller.distanceTravelledLabel.text = @"0.0 Kilometres"; controller.tripDurationLabel.text = @"00:00:00"; } controller.onOffSwitch.on = isUpdating; } - (void)saveTrip { [NSKeyedArchiver archiveRootObject:locationHistory toFile:fileName]; } - (void)loadTrip { [self cancelGeocoding]; [self removeMapMarkup]; locationHistory = nil; locationHistory = [NSKeyedUnarchiver unarchiveObjectWithFile:fileName]; if(![locationHistory count]) { return; } for(SpotAnnotation *annotation in locationHistory) { [mapView addAnnotation:annotation]; } currentPointAnnotation = nil; SpotAnnotation *lastAnnotation = [locationHistory objectAtIndex:[locationHistory count] - 1]; [mapView removeAnnotation:lastAnnotation]; currentPointAnnotation = [[SpotAnnotation alloc] initWithState:CURRENT_SPOT coordinate:lastAnnotation.coordinate andDate:lastAnnotation.spotDate]; [mapView addAnnotation:currentPointAnnotation]; [mapView setRegion:MKCoordinateRegionMakeWithDistance(currentPointAnnotation.coordinate, 1000.0, 1000.0) animated:YES]; mapRegionSet = YES; //calculate trip distance

Nicolaas tenBroek (AMDG)

467

iOS Development MainViewController.m (section 4 of 6)

Device Location

SpotAnnotation *annotationOne = [locationHistory objectAtIndex:0]; CLLocation *locOne = [[CLLocation alloc] initWithLatitude:annotationOne.coordinate.latitude longitude:annotationOne.coordinate.longitude]; for(int i = 1; i < [locationHistory count]; i++) { SpotAnnotation *annotationTwo = [locationHistory objectAtIndex:i]; CLLocation *locTwo = [[CLLocation alloc] initWithLatitude:annotationTwo.coordinate.latitude longitude:annotationTwo.coordinate.longitude]; distanceTotal += [locTwo distanceFromLocation:locOne]; locOne = locTwo; } locOne = nil; //get placemark for starting location CLLocationCoordinate2D coordinate = [[locationHistory objectAtIndex:0] coordinate]; [self reverseGeocodeLocation:[[CLLocation alloc] initWithLatitude:coordinate.latitude longitude:coordinate.longitude]];

The next section contains the classs private methods, and again these are simple and should be familiar. We begin with the battery-state monitoring which adjusts the accuracy of the location information by the battery state, just as we did in our previous location example. While this code demonstrates Apples recommended behaviour for location tracking, you may have real-world needs that would call for ignoring the recommendation. For instance, an exercise app would provide the best location and time data using an accuracy level of kCLLocationAccuracyBestForNavigation, but such apps are rarely if ever used while the device is plugged in. Of course, Apple has made the recommendation because that level of accuracy requires the most power and will quickly deplete a battery. In this scenario then, it might be best to offer the user the option to enable the high accuracy, but they should be clearly warned that such a setting would drain the battery rather quickly. Next we have the removeMapMarkup method that simply clears all of the annotations and overlays from the map. Note that the map might not handle nil values being passed to the remove methods, so we check each variable before using it. MainViewController.m (section 5 of 6)
#pragma mark - Miscellaneous Helper Methods - (void)setAccuracyByBatteryState { UIDevice *device = [UIDevice currentDevice]; if(device.batteryState == UIDeviceBatteryStateUnknown || device.batteryState == UIDeviceBatteryStateUnplugged) { locationManager.desiredAccuracy = kCLLocationAccuracyBest; } else { locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation; }

Nicolaas tenBroek (AMDG)

468

iOS Development MainViewController.m (section 5 of 6)


} - (void)removeMapMarkup { for(SpotAnnotation *annotation in locationHistory) { [mapView removeAnnotation:annotation]; } [locationHistory removeAllObjects]; if(startingPlacemark) { [mapView removeAnnotation:startingPlacemark]; startingPlacemark = nil; } if(currentLocationCircle) { [mapView removeOverlay:currentLocationCircle]; currentLocationCircle = nil; } if(currentPointAnnotation) { [mapView removeAnnotation:currentPointAnnotation]; currentPointAnnotation = nil; } mapRegionSet = NO; distanceTotal = 0.0;

Device Location

- (void)batteryStateChanged:(NSNotification *)notification { if(isUpdating) { [self setAccuracyByBatteryState]; } }

The final section of this class contains the View Lifecycle methods, which are all quite standard. In fact, the only part that does not contain code you have already seen many times before is the initWithNibName:bundle: in which we set up the location manager; though even this code is nearly the same as we used in the previous location example. MainViewController.m (section 6 of 6)
#pragma mark - View Lifecycle - (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle { if((self = [super initWithNibName:nibName bundle:bundle])) { locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; locationManager.headingOrientation = CLDeviceOrientationFaceUp; locationManager.purpose = @"Demonstration of Map Kit";

Nicolaas tenBroek (AMDG)

469

iOS Development MainViewController.m (section 6 of 6)

Device Location

} }

locationHistory = [[NSMutableArray alloc] init]; fileName = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"LocationHistory"];

return self;

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // Return YES for supported orientations. return (interfaceOrientation == UIInterfaceOrientationPortrait); } @end

Nicolaas tenBroek (AMDG)

470

iOS Development

Device Location

The FlipsideViewController was merely a convenient place to hold the UI controls we wished to use, so it is quite simple in design. We have several IBOutlet properties and IBAction methods to handle user interaction, and the FlipsideViewControllerDelegate protocol declaration. FlipsideViewController.h
#import <UIKit/UIKit.h> #import <MapKit/MapKit.h> @class FlipsideViewController; @protocol FlipsideViewControllerDelegate - (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller; - (void)setMapType:(MKMapType)mapType; - (void)beginLocationUpdates; - (void)endLocationUpdates; - (void)saveTrip; - (void)loadTrip; @end @interface FlipsideViewController : UIViewController @property @property @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, weak) weak) weak) weak) weak) weak) IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet id <FlipsideViewControllerDelegate> delegate; UISegmentedControl *mapTypeSegmentedControl; UILabel *distanceTravelledLabel; UILabel *tripDurationLabel; UILabel *averageSpeedLabel; UISwitch *onOffSwitch;

(IBAction)done:(id)sender; (IBAction)mapTypeChanged; (IBAction)switchValueChanged; (IBAction)saveTrip; (IBAction)loadTrip;

@end

The implementation of the FlipsideViewController is as simple as the interface. Most of the code simply responds to the users action by passing messages on to the MainViewController and thus to the map. FlipsideViewController.m
#import "FlipsideViewController.h" @implementation FlipsideViewController @synthesize @synthesize @synthesize @synthesize @synthesize delegate; mapTypeSegmentedControl; distanceTravelledLabel; tripDurationLabel; averageSpeedLabel;

Nicolaas tenBroek (AMDG)

471

iOS Development FlipsideViewController.m


@synthesize onOffSwitch; #pragma mark - Actions - (IBAction)done:(id)sender { [self.delegate flipsideViewControllerDidFinish:self]; } - (IBAction)mapTypeChanged { [self.delegate setMapType:mapTypeSegmentedControl.selectedSegmentIndex]; } - (IBAction)switchValueChanged { if(onOffSwitch.on) { [self.delegate beginLocationUpdates]; } else { [self.delegate endLocationUpdates]; } } - (IBAction)saveTrip { [self.delegate saveTrip]; } - (IBAction)loadTrip { [self.delegate loadTrip]; [self done:nil]; } @end

Device Location

Finally we have a portion of the app delegate, which should be quite familiar to you by now. This code ensures that the location updates are disabled when the app is pushed to the background, and though we are not using it, the code to reactivate location updates is provided here as well as a reminder. We also enable and disable the idle timer which keeps the screen from turning off while we are using our app. After all, a black screen is not terribly helpful as a navigational aid. While simple, these steps are crucial and should not be overlooked. AppDelegate.m
- (void)applicationWillResignActive:(UIApplication *)application { [self.mainViewController endLocationUpdates]; [[UIApplication sharedApplication] setIdleTimerDisabled:NO]; } - (void)applicationDidBecomeActive:(UIApplication *)application { // [self.mainViewController beginLocationUpdates]; [[UIApplication sharedApplication] setIdleTimerDisabled:YES]; }

Nicolaas tenBroek (AMDG)

472

iOS Development

Device Location

Here are some screen shots of the app in use. The first image shows the app running with the orange starting spot and the reverse geocoded pin marking the starting location. The multi-coloured spot at the shows the current location and the blue circle overlay reveals the relative accuracy of the readings.

The second image shows the end of a route after it has been loaded from the history. Note that the blue circle overlay is not present.

By now you should be quite comfortable with accessing the device location data. Be sure to remember that while you are free to access such data within your app, you must handle the data in a responsible and ethical manner. Ask others what they think about how you will be handling the data and see if they would be comfortable with it before you make firm decisions that will be costly to undo.

Nicolaas tenBroek (AMDG)

473

iOS Development

Device Location

Nicolaas tenBroek (AMDG)

474

You might also like