The Basics of Object Oriented Programming in Objective-C (iOS 5)
Previous | Table of Contents | Next |
The Basics of Objective-C Programming | An Overview of the iPad iOS 5 Application Development Architecture |
Learn SwiftUI and take your iOS Development to the Next Level |
Objective-C provides extensive support for developing object-oriented iOS iPad applications. The subject area of object oriented programming is, however, large. It is not an exaggeration to state that entire books have been dedicated to the subject. As such, a detailed overview of object oriented software development is beyond the scope of this book. Instead, we will introduce the basic concepts involved in object oriented programming and then move on to explaining the concept as it relates to Objective-C application development. Once again, whilst we strive to provide the basic information you need in this chapter, we recommend reading a copy of Objective-C 2.0 Essentials if you are unfamiliar with Objective-C programming.
What is an Object?
Objects are self-contained modules of functionality that can be easily used, and re-used as the building blocks for a software application.
Objects consist of data variables and functions (called methods) that can be accessed and called on the object to perform tasks. These are collectively referred to as members.
What is a Class?
Much as a blueprint or architect's drawing defines what an item or a building will look like once it has been constructed, a class defines what an object will look like when it is created. It defines, for example, what the methods will do and what the member variables will be.
Declaring an Objective-C Class Interface
Before an object can be instantiated we first need to define the class 'blueprint' for the object. In this chapter we will create a Bank Account class to demonstrate the basic concepts of Objective-C object oriented programming.
An Objective-C class is defined in terms of an interface and an implementation. In the interface section of the definition we specify the base class from which the new class is derived and also define the members and methods that the class will contain. The syntax for the interface section of a class is as follows:
@interface NewClassName: ParentClass { ClassMembers; } ClassMethods; @end
The ClassMembers section of the interface defines the variables that are to be contained within the class (also referred to as instance variables). These variables are declared in the same way that any other variable would be declared in Objective-C.
The ClassMethods section defines the methods that are available to be called on the class. These are essentially functions specific to the class that perform a particular operation when called upon.
To create an example outline interface section for our BankAccount class, we would use the following:
@interface BankAccount: NSObject { } @end
The parent class chosen above is the NSObject class. This is a standard base class provided with the Objective-C Foundation framework and is the class from which most new classes are derived. By deriving BankAccount from this parent class we inherit a range of additional methods used in creating, managing and destroying instances that we would otherwise have to write ourselves.
Now that we have the outline syntax for our class, the next step is to add some instance variables to it.
Adding Instance Variables to a Class
A key goal of object oriented programming is a concept referred to as data encapsulation. The idea behind data encapsulation is that data should be stored within classes and accessed only through methods defined in that class. Data encapsulated in a class are referred to as instance variables.
Instances of our BankAccount class will be required to store some data, specifically a bank account number and the balance currently held by the account. Instance variables are declared in the same way any other variables are declared in Objective-C. We can, therefore, add these variables as follows:
@interface BankAccount: NSObject { double accountBalance; long accountNumber; } @end
Having defined our instance variables, we can now move on to defining the methods of the class that will allow us to work with our instance variables while staying true to the data encapsulation model.
Define Class Methods
The methods of a class are essentially code routines that can be called upon to perform specific tasks within the context of an instance of that class.
Methods come in two different forms, class methods and instance methods. Class methods operate at the level of the class, such as creating a new instance of a class. Instance methods, on the other hand, operate only on the instance of a class (for example performing an arithmetic operation on two instance variables and returning the result). Class methods are preceded by a plus (+) sign in the declaration and instance methods are preceded by a minus (-) sign. If the method returns a result, the name of method must be preceded by the data type returned enclosed in parentheses. If a method does not return a result, then the method must be declared as void. If data needs to be passed through to the method (referred to as arguments), the method name is followed by a colon, the data type in parentheses and a name for the argument. For example, the declaration of a method to set the account number in our example might read as follows:
-(void) setAccountNumber: (long) y;
The method is an instance method so it is preceded by the minus sign. It does not return a result so it is declared as (void). It takes an argument (the account number) of type long so we follow the accountNumber name with a colon (:) specify the argument type (long) and give the argument a name (in this case we simply use y).
The following method is intended to return the current value of the account number instance variable (which is of type long):
-(long) getAccountNumber;
Methods may also be defined to accept more than one argument. For example to define a method that accepts both the account number and account balance we could declare it as follows:
-(void) setAccount: (long) y andBalance: (double) x;
Now that we have an understanding of the structure of method declarations within the context of the class interface definition, we can extend our BankAccount class accordingly:
@interface BankAccount: NSObject { double accountBalance; long accountNumber; } -(void) setAccount: (long) y andBalance: (double) x; -(void) setAccountBalance: (double) x; -(double) getAccountBalance; -(void) setAccountNumber: (long) y; -(long) getAccountNumber; -(void) displayAccountInfo; @end
Having defined the interface, we can now move on to defining the implementation of our class.
Declaring an Objective-C Class Implementation
The next step in creating a new class in Objective-C is to write the code for the methods we have already declared. This is performed in the @implementation section of the class definition. An outline implementation is structured as follows:
@implementation NewClassName ClassMethods @end
In order to implement the methods we declared in the @interface section, therefore, we need to write the following code:
@implementation BankAccount -(void) setAccount: (long) y andBalance: (double) x; { accountBalance = x; accountNumber = y; } -(void) setAccountBalance: (double) x { accountBalance = x; } -(double) getAccountBalance { return accountBalance; } -(void) setAccountNumber: (long) y { accountNumber = y; } -(long) getAccountNumber { return accountNumber; } -(void) displayAccountInfo { NSLog (@"Account Number %i has a balance of %f", accountNumber, accountBalance); } @end
We are now at the point where we can write some code to work with our new BankAccount class.
Declaring and Initializing a Class Instance
So far all we have done is define the blueprint for our class. In order to do anything with this class, we need to create instances of it. The first step in this process is to declare a variable to store a pointer to the instance when it is created. We do this as follows:
BankAccount *account1;
Having created a variable to store a reference to the class instance, we can now allocate memory in preparation for initializing the class:
account1 = [BankAccount alloc];
In the above statement we are calling the alloc method of the BankAccount class (note that alloc is a class method inherited from the parent NSObject class, as opposed to an instance method created by us in the BankAccount class).
Having allocated memory for the class instance, the next step is to initialize the instance by calling the init instance method:
account1 = [account1 init];
For the sake of economy of typing, the above three statements are frequently rolled into a single line of code as follows:
BankAccount *account1 = [[BankAccount alloc] init];
Automatic Reference Counting (ARC)
Whilst the ARC avoids the necessity to call the release method of an object it is still, however, recommended that any strong outlet references be assigned nil in the viewDidUnload methods of your view controllers to improve memory usage efficiency. As a result, examples in this book will follow this convention where appropriate.
When creating a new project, Xcode now provides the option to implement automatic reference counting in the application code. If this option is selected, the code should not make calls to release, retain, autorelease or dealloc methods. Management of objects at this level is now handled for you by ARC.
Calling Methods and Accessing Instance Data
Given the length of this chapter, now is probably a good time to recap what we have done so far. We have now created a new class called BankAccount. Within this new class we declared some instance variables to contain the bank account number and current balance together with some instance methods used to set, get and display these values. In the preceding section we covered the steps necessary to create and initialize an instance of our new class. The next step is to learn how to call the instance methods we built into our class.
The syntax for invoking methods is to place the object pointer variable name and method to be called in square brackets ([]). For example, to call the displayAccountInfo method on the instance of the class we created previously we would use the following syntax:
[account1 displayAccountInfo];
When the method accepts a single argument, the method name is followed by a colon (:) followed by the value to be passed to the method. For example, to set the account number:
[account1 setAccountNumber: 34543212];
In the case of methods taking multiple arguments (as is the case with our setAccount method) syntax similar to the following is employed:
[account1 setAccount: 4543455 andBalance: 3010.10];
Objective-C and Dot Notation
Those familiar with object oriented programming in Java, C++ or C# are probably reeling a little from the syntax used in Objective-C. They are probably thinking life was much easier when they could just use something called dot notation to set and get the values of instance variables. The good news is that one of the features introduced into version 2.0 of Objective-C is support for dot notation.
Dot notation involves accessing an instance variable by specifying a class instance followed by a dot followed in turn by the name of the instance variable or property to be accessed:
classinstance.property
For example, to the get current value of our accountBalance instance variable:
double balance1 = account1.accountBalance;
Dot notation can also be used to set values of instance properties:
account1.accountBalance = 6789.98;
A key point to understand about dot notation is that it only works for instance variables for which synthesized accessor methods have been declared. If you attempt to use dot notation to access an instance variable for which no synthesized accessor is available the code will fail to compile with an error similar to:
error: request for member 'accountBalance' in something not a structure or union
How Variables are Stored
When we declare a variable in Objective-C and assign a value to it we are essentially allocating a location in memory where that value is stored. Take, for example, the following variable declaration:
int myvar = 10;
When the above code is executed, a block of memory large enough to hold an integer value is reserved in memory and the value of 10 is placed at that location. Whenever we reference this variable in code, we are actually using the variable value. For example, the following code adds the value of myvar (i.e. 10) to the constant value 20 to arrive at a result of 30.
int result = 20 + myvar;
Similarly, when we pass a variable through as an argument to a method or function we are actually passing the value of the variable, not the variable itself. To better understand this concept, consider the following sample program:
#import <Foundation/Foundation.h> void myFunction(int i) { i = i + 10; } int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int myvar = 10; NSLog (@"Before call to function myvar = %i", myvar); myFunction (myvar); NSLog (@"After call to function myvar = %i", myvar); [pool drain]; return 0; }
The above program consists of a main function that declares our myvar variable and displays the current value. It then calls the function myFunction passing through the value of the myvar variable. The myFunction function adds 10 to the value it was passed as an argument and then returns to the main function where the value of myvar is once again displayed. When compiled and executed the following output is displayed:
Before call to function myvar = 10 After call to function myvar = 10
Clearly, even though the value passed through to myFunction was increased by 20 the value of myvar remained unchanged. This is because what was passed through as an argument to myFunction was the value of myvar, not the myvar variable itself. Therefore, in myFunction we were simply working on a constant value of 10 that had absolutely no connection to the original myvar variable.
In order to be able to work on the actual variable in the function we need to use something called indirection.
An Overview of Indirection
Indirection involves working with pointers to the location of variables and objects rather than the contents of those items. In other words, instead of working with the value stored in a variable, we work with a pointer to the memory address where the variable is located.
Pointers are declared by prefixing the name with an asterisk (*) character. For example to declare a pointer to our myvar variable we would write the following code:
int myvar = 10; int *myptr;
In the above example we have declared our myvar variable and then declared a variable named myptr as being of type pointer to an integer. Having declared both our variable and our pointer we now need to assign the address of our variable to the pointer. The address of a variable is referenced by prefixing it with the ampersand (&) character. We can, therefore, extend our example to assign the address of the myvar variable to the myptr variable:
int myvar = 10; int *myptr; myptr = &myvar;
We now have now implemented a level of indirection by creating a pointer to our variable. As such, we can now pass this pointer through as an argument to our function such that we will be able to work on the actual variable, rather than just the value (10) of the variable. In order to access the value of a variable using a pointer to that variable, we prefix the pointer variable name with an asterisk (*). When we do this we are telling the compiler we want to work with the contents of the variable or object at the memory address contained within the pointer:
int myvar = 10; int *myptr; myptr = &myvar; *myptr = *myptr + 15;
Similarly, we can modify our function to accept a pointer to an integer and perform the addition on that variable. As such, we can now modify our previous program as follows:
#import <Foundation/Foundation.h> void myFunction(int *i) { *i = *i + 10; } int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int myvar = 10; int *myptr; myptr = &myvar; NSLog (@"Before call to function myvar = %i", myvar); myFunction (myptr); NSLog (@"After call to function myvar = %i", myvar); [pool drain]; return 0; }
Now because we are passing through a pointer to myvar when we call the function and have modified the function to work with the contents of the variable, the output clearly indicates that the function changed the value of myvar when it was called. We have, therefore, just used indirection.
Before call to function myvar = 10 After call to function myvar = 20
Indirection and Objects
So far in this chapter we have used indirection with a variable. The same concept applies for objects. In previous chapters we worked with our BankAccount class. When doing so we wrote statements similar to the following:
BankAccount *account1; BankAccount *account1 = [[BankAccount alloc] init];
The first line of code (BankAccount *account1;) is actually declaring that the variable named account1 is a pointer to an object of type BankAccount. We are, therefore, using indirection to provide a handle to our object. The calls to the alloc and init methods subsequently create the object in memory and the assign the address of that object to the account1 pointer variable. We are, therefore, using indirection once again.
One key point to note is that we do not need to prefix the object pointer with a * when perform operations such as calling methods. For example, we can call a method on our account1 object without using an asterisk:
[account1 displayAccountInfo];
Indirection and Object Copying
Due to the fact that references to objects utilize indirection it is important to understand that when we use the assignment operator (=) to assign one object to another we are not actually creating a copy of the object. Instead, we are creating a copy of the pointer to the object. Consider, therefore, the following code:
BankAccount *account1; BankAccount *account2; BankAccount *account1 = [[BankAccount alloc] init]; account2 = account1;
In the above example, we will end up with two pointers (account1 and account2) that point to the same location in memory.
Creating the Program Section
The last stage in this exercise is to bring together all the components we have created so that we can actually see the concept working. The last section we need to look at is called the program section. This is where we write the code to create the class instance and call the instance methods. Most Objective-C programs have a main() routine which is the start point for the application. The following sample main routine creates an instance of our class and calls the methods we created:
int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // Create a variable to point to our class instance BankAccount *account1; // Allocate memory for class instance account1 = [BankAccount alloc]; // Initialize the instance account1 = [account1 init]; // Set the account balance [account1 setAccountBalance: 1500.53]; // Set the account number [account1 setAccountNumber: 34543212]; // Call the method to display the values of // the instance variables [account1 displayAccountInfo]; // Set both account number and balance [account1 setAccount: 4543455 andBalance: 3010.10]; // Output values using the getter methods NSLog(@"Number = %i, Balance = %f", [account1 getAccountNumber], [account1 getAccountBalance]); [pool drain]; return 0; }
Bringing it all Together
Our example is now complete so let’s bring all the components together:
#import <Foundation/Foundation.h> // Interface Section Starts Here @interface BankAccount: NSObject { double accountBalance; long accountNumber; } -(void) setAccount: (long) y andBalance: (double) x; -(double) getAccountBalance; -(long) getAccountNumber; -(void) setAccountBalance: (double) x; -(void) setAccountNumber: (long) y; -(void) displayAccountInfo; @end // Implementation Section Starts Here @implementation BankAccount -(void) setAccount: (long) y andBalance: (double) x; { accountBalance = x; accountNumber = y; } -(void) setAccountBalance: (double) x { accountBalance = x; } -(double) getAccountBalance { return accountBalance; } -(void) setAccountNumber: (long) y { accountNumber = y; } -(long) getAccountNumber { return accountNumber; } -(void) displayAccountInfo { NSLog (@"Account Number %i has a balance of %f", accountNumber, accountBalance); } @end // Program Section Starts Here int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; BankAccount *account1; account1 = [BankAccount alloc]; account1 = [account1 init]; [account1 setAccountBalance: 1500.53]; [account1 setAccountNumber: 34543212]; [account1 displayAccountInfo]; [account1 setAccount: 4543455 andBalance: 3010.10]; NSLog(@"Number = %i, Balance = %f", [account1 getAccountNumber], [account1 getAccountBalance]); [pool drain]; return 0; }
When the above code is saved, compiled and executed we should expect to see the following output:
2009-10-14 14:44:06.634 t[4287:10b] Account Number 34543212 has a balance of 1500.530000 2009-10-14 14:44:06.635 t[4287:10b] Number = 4543455, Balance = 3010.100000
Structuring Object-Oriented Objective-C Code
Our example is currently contained within a single source file. In practice, the convention is to place the interface and implementation in their own include files that are then included in the program source file. Generally the interface section is contained within a file called ClassName.h where ClassName is the name of the class. In our case, we would create a file called BankAccount.h containing the following:
#import <Foundation/Foundation.h> @interface BankAccount: NSObject { double accountBalance; long accountNumber; } -(void) setAccount: (long) y andBalance: (double) x; -(double) getAccountBalance; -(long) getAccountNumber; -(void) setAccountBalance: (double) x; -(void) setAccountNumber: (long) y; -(void) displayAccountInfo; @end
Next, the implementation section goes in a file traditionally named ClassName.m where ClassName once again is refers to the name of the class. For example, BankAccount.m will contain the following (note that it is necessary to import the BankAccount.h file into this file):
#import "BankAccount.h" @implementation BankAccount -(void) setAccount: (long) y andBalance: (double) x; { accountBalance = x; accountNumber = y; } -(void) setAccountBalance: (double) x { accountBalance = x; } -(double) getAccountBalance { return accountBalance; } -(void) setAccountNumber: (long) y { accountNumber = y; } -(long) getAccountNumber { return accountNumber; } -(void) displayAccountInfo { NSLog (@"Account Number %i has a balance of %f", accountNumber, accountBalance); } @end
Finally, we will create our program file and call it bank.m (though any suitable name will do as long as it has a .m filename extension). This file also needs to import our interface file (BankAccount.h):
#import "BankAccount.h" int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; BankAccount *account1; account1 = [BankAccount alloc]; account1 = [account1 init]; [account1 setAccountBalance: 1500.53]; [account1 setAccountNumber: 34543212]; [account1 displayAccountInfo]; [account1 setAccount: 4543455 andBalance: 3010.10]; NSLog(@"Number = %i, Balance = %f", [account1 getAccountNumber], [account1 getAccountBalance]); [pool drain]; return 0; }
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
The Basics of Objective-C Programming | An Overview of the iPad iOS 5 Application Development Architecture |