You’re reading Ry’s Objective-C Tutorial |
Classes
As in many other object-oriented programming language, Objective-C classes provide the blueprint for creating objects. First, you define a reusable set of properties and behaviors inside of a class. Then, you instantiate objects from that class to interact with those properties and behaviors.
Objective-C is similar to C++ in that it abstracts a class’s interface from its implementation. An interface declares the public properties and methods of a class, and the corresponding implementation defines the code that actually makes these properties and methods work. This is the same separation of concerns that we saw with functions.
In this module, we’ll explore the basic syntax for class interfaces, implementations, properties, and methods, as well as the canonical way to instantiate objects. We’ll also introduce some of Objective-C’s introspection and reflection capabilities.
Creating Classes
We’ll be working with a class called Car
whose interface
resides in a file named Car.h
(also called a “header”)
and whose implementation resides in Car.m
. These are the standard
file extensions for Objective-C classes. The class’s header file is what
other classes use when they need to interact with it, and its implementation
file is used only by the compiler.
Xcode provides a convenient template for creating new classes. To create our
Car
class, navigate to File > New > File… or
use the Cmd+N shortcut. For the template, choose Objective-C
class under the iOS > Cocoa Touch category. After that,
you’ll be presented with some configuration options:
Use Car
for the Class field and NSObject
for the Subclass of field. NSObject
is Objective-C’s top-level class from which almost all others inherit.
Clicking Next will prompt you to select a location for the file. Save
it in the top-level of the project directory. At the bottom of that dialog,
make sure that your project is checked in the Targets section. This is
what actually adds the class to the list of compiled sources.
After clicking Next, you should see new Car.h
and
Car.m
files in Xcode’s Project Navigator. If you
select the project name in the navigator, you’ll also find
Car.m
in the Build Phases tab under the Compile
Sources section. Any files that you want the compiler to see must be in
this list (if you didn’t create your source files through Xcode, this is
where you can manually add them).
Interfaces
Car.h
contains some template code, but let’s go ahead and
change it to the following. This declares a property called model
and a method called drive
.
// Car.h
#import
<Foundation/Foundation.h>
@interface
Car
:NSObject
{
// Protected instance variables (not recommended)
}
@property
(
copy
)
NSString
*
model
;
-
(
void
)
drive
;
@end
An interface is created with the @interface
directive, after
which come the class and the superclass name, separated by a colon. Protected
variables can be defined inside of the curly braces, but most developers treat
instance variables as implementation details and prefer to store them in the
.m
file instead of the interface.
The @property
directive declares a public property, and the
(copy)
attribute defines its memory management behavior. In this
case, the value assigned to model
will be stored as a copy instead
of a direct pointer. The Properties module
discusses this in more detail. Next come the property’s data type and
name, just like a normal variable declaration.
The -(void)drive
line declares a method called
drive
that takes no parameters, and the (void)
portion defines its return type. The minus sign prepended to the method marks
it as an instance method (opposed to a class method).
Implementations
The first thing any class implementation needs to do is import its
corresponding interface. The @implementation
directive is similar
to @interface
, except you don’t need to include the super
class. Private instance variables can be stored between curly braces after the
class name:
// Car.m
#import
"Car.h"
@implementation
Car
{
// Private instance variables
double
_odometer
;
}
@synthesize
model
=
_model
;
// Optional for Xcode 4.4+
-
(
void
)
drive
{
NSLog
(
@"Driving a %@. Vrooooom!"
,
self
.
model
);
}
@end
@synthesize
is a convenience directive that automatically
generates accessor methods for the property. By default, the getter is simply
the property name (model
), and the setter is the capitalized name
with the set
prefix (setModel
). This is much easier
than manually creating accessors for every property. The _model
portion of the synthesize statement defines the private instance variable name
to use for the property.
As of Xcode 4.4, properties declared with @property
will be
automatically synthesized, so it’s safe to omit the
@synthesize
line if you’re ok with the default instance
variable naming conventions.
The drive
implementation has the same signature as the
interface, but it’s followed by whatever code should be executed when the
method is called. Note how we accessed the value via self.model
instead of the _model
instance variable. This is a best practice
step because it utilizes the property’s accessor methods. Typically, the only place
you’ll need to directly access instance variables is in init
methods and the dealloc
method.
The self
keyword refers to the instance calling the method
(like this
in C++ and Java). In addition to accessing properties,
this can be used to call other methods defined on the same class (e.g.,
[self anotherMethod]
). We’ll see many examples of this
throughout the tutorial.
Instantiation and Usage
Any files that need access to a class must import its header file
(Car.h
)—they should never, ever try to access the
implementation file directly. That would defeat the goal of separating the
public API from its underlying implementation. So, to see our Car
class in action, change main.m
to the following.
// main.m
#import
<Foundation/Foundation.h>
#import
"Car.h"
int
main
(
int
argc
,
const
char
*
argv
[])
{
@autoreleasepool
{
Car
*
toyota
=
[[
Car
alloc
]
init
];
[
toyota
setModel:
@"Toyota Corolla"
];
NSLog
(
@"Created a %@"
,
[
toyota
model
]);
toyota
.
model
=
@"Toyota Camry"
;
NSLog
(
@"Changed the car to a %@"
,
toyota
.
model
);
[
toyota
drive
];
}
return
0
;
}
After the interface has been imported with the #import
directive, you can instantiate objects with the
alloc
/init
pattern shown above. As you can see,
instantiation is a two-step process: first you must allocate some memory for
the object by calling the alloc
method, then you need to
initialize it so it’s ready to use. You should never use an uninitialized
object.
It’s worth repeating that all
objects must be stored as pointers. This is why we used Car
*toyota
instead of Car toyota
to declare the variable.
To call a method on an Objective-C object, you place the instance and the
method in square brackets, separated by a space. Arguments are passed after the
method name, preceded by a colon. So, if you’re coming from a C++, Java,
or Python background, the [toyota setModel:@"Toyota Corolla"]
call would translate to:
toyota
.
setModel
(
"Toyota Corolla"
);
This square-bracket syntax can be unsettling for newcomers to the language, but rest assured, you’ll be more than comfortable with Objective-C’s method conventions after reading through the Methods module.
This example also shows you both ways to work with an object’s
properties. You can either use the synthesized model
and
setModel
accessor methods, or you can use the convenient
dot-syntax, which should be more familiar to developers who have been using
Simula-style languages.
Class Methods and Variables
The above snippets define instance-level properties and methods, but
it’s also possible to define class-level ones. These are commonly called
“static” methods/properties in other programming languages (not to
be confused with the static
keyword).
Class method declarations look just like instance methods, except they are
prefixed with a plus sign instead of a minus sign. For example, let’s add
the following class-level method to Car.h
:
// Car.h
+
(
void
)
setDefaultModel:
(
NSString
*
)
aModel
;
Similarly, a class method implementation is also preceded by a plus
sign. While there is technically no such thing as a class-level variable in
Objective-C, you can emulate one by declaring a static
variable
before defining the implementation:
// Car.m
#import
"Car.h"
static
NSString
*
_defaultModel
;
@implementation
Car
{
...
+
(
void
)
setDefaultModel:
(
NSString
*
)
aModel
{
_defaultModel
=
[
aModel
copy
];
}
@end
The [aModel copy]
call creates a copy of the parameter instead
of assigning it directly. This is what’s going on under the hood when we
used the (copy)
attribute on the model
property.
Class methods use the same square-bracket syntax as instance methods, but
they must be called directly on the class, as shown below. They cannot
be called on an instance of that class ([toyota setDefaultModel:@"Model
T"]
will throw an error).
// main.m
[
Car
setDefaultModel:
@"Nissan Versa"
];
“Constructor” Methods
There are no constructor methods in Objective-C. Instead, an object is
initialized by calling the init
method
immediately after it’s allocated. This is why instantiation is always a
two-step process: allocate, then initialize. There is also a class-level
initialization method that will be discussed in a moment.
init
is the default initialization method, but you can also
define your own versions to accept configuration parameters. There’s
nothing special about custom initialization methods—they’re just
normal instance methods, except the method name should always begin with
init
. An exemplary “constructor” method is shown
below.
// Car.h
-
(
id
)
initWithModel:
(
NSString
*
)
aModel
;
To implement this method, you should follow the canonical initialization
pattern shown in initWithModel:
below. The super
keyword refers to the parent class, and again, the self
keyword
refers to the instance calling the method. Go ahead and add the following
methods to Car.m
.
// Car.m
-
(
id
)
initWithModel:
(
NSString
*
)
aModel
{
self
=
[
super
init
];
if
(
self
)
{
// Any custom setup work goes here
_model
=
[
aModel
copy
];
_odometer
=
0
;
}
return
self
;
}
-
(
id
)
init
{
// Forward to the "designated" initialization method
return
[
self
initWithModel:
_defaultModel
];
}
Initialization methods should always return a reference to the object
itself, and if it cannot be initialized, it should return nil
.
This is why we need to check if self
exists before trying to use
it. There should typically only be one initialization method that needs to do
this, and the rest should forward calls to this designated
initializer. This eliminates boilerplate code when you have several
custom init
methods.
Also notice how we directly assigned values to the _model
and
_odometer
instance variables in initWithModel:
.
Remember that this is one of the only places you should do this—in the
rest of your methods you should be using self.model
and
self.odometer
.
Class-Level Initialization
The initialize
method is the class-level equivalent of
init
. It gives you a chance to set up the class before anyone uses
it. For example, we can use this to populate the _defaultModel
variable with a valid value, like so:
// Car.m
+
(
void
)
initialize
{
if
(
self
==
[
Car
class
])
{
// Makes sure this isn't executed more than once
_defaultModel
=
@"Nissan Versa"
;
}
}
The initialize
class method is called once for every class
before the class is used. This includes all subclasses of Car
,
which means that Car
will get two initialize
calls if
one of its subclasses didn’t re-implement it. As a result, it’s
good practice to use the self == [Car class]
conditional to ensure that the initialization code is only run once. Also note
that in class methods, the self
keyword refers to the class
itself, not an instance.
Objective-C doesn’t force you to mark methods as overrides. Even
though init
and initialize
are both defined by its
superclass, NSObject
, the compiler won’t complain when you
redefine them in Car.m
.
The next iteration of main.m
shows our custom initialization
methods in action. Before the first time the class is used, [Car
initialize]
is called automatically, setting _defaultModel
to @"Nissan Versa"
. This can be seen in the first
NSLog()
. You can also see the result of the custom initialization
method (initWithModel:
) in the second log output.
// main.m
#import
<Foundation/Foundation.h>
#import
"Car.h"
int
main
(
int
argc
,
const
char
*
argv
[])
{
@autoreleasepool
{
// Instantiating objects
Car
*
nissan
=
[[
Car
alloc
]
init
];
NSLog
(
@"Created a %@"
,
[
nissan
model
]);
Car
*
chevy
=
[[
Car
alloc
]
initWithModel:
@"Chevy Corvette"
];
NSLog
(
@"Created a %@, too."
,
chevy
.
model
);
}
return
0
;
}
Dynamic Typing
Classes themselves are represented as objects, which makes it possible to query their properties (introspection), or even change their behavior on-the-fly (reflection). These are very powerful dynamic typing capabilities, as they let you call methods and set properties on objects even when you don’t know what type of object they are.
The easiest way to get a class object is via the class
class-level method (apologies for the redundant terminology). For example,
[Car class]
returns an object representing the Car
class. You can pass this object around to methods like
isMemberOfClass:
and isKindOfClass:
to get
information about other instances. A comprehensive example is included
below.
// main.m
#import
<Foundation/Foundation.h>
#import
"Car.h"
int
main
(
int
argc
,
const
char
*
argv
[])
{
@autoreleasepool
{
Car
*
delorean
=
[[
Car
alloc
]
initWithModel:
@"DeLorean"
];
// Get the class of an object
NSLog
(
@"%@ is an instance of the %@ class"
,
[
delorean
model
],
[
delorean
class
]);
// Check an object against a class and all subclasses
if
([
delorean
isKindOfClass:
[
NSObject
class
]])
{
NSLog
(
@"%@ is an instance of NSObject or one "
"of its subclasses"
,
[
delorean
model
]);
}
else
{
NSLog
(
@"%@ is not an instance of NSObject or "
"one of its subclasses"
,
[
delorean
model
]);
}
// Check an object against a class, but not its subclasses
if
([
delorean
isMemberOfClass:
[
NSObject
class
]])
{
NSLog
(
@"%@ is a instance of NSObject"
,
[
delorean
model
]);
}
else
{
NSLog
(
@"%@ is not an instance of NSObject"
,
[
delorean
model
]);
}
// Convert between strings and classes
if
(
NSClassFromString
(
@"Car"
)
==
[
Car
class
])
{
NSLog
(
@"I can convert between strings and classes!"
);
}
}
return
0
;
}
The NSClassFromString()
function is an alternative way to get
your hands on a class object. This is very flexible, as it lets you dynamically
request class objects at runtime; however, it’s also rather inefficient.
For this reason, you should opt for the class
method whenever
possible.
If you’re interested in dynamic typing, be sure to check out Selectors and The id
Type.
Summary
In this module, we learned how to create classes, instantiate objects, define initialization methods, and work with class-level methods and variables. We also took a brief look at dynamic typing.
The previous module
mentioned that Objective-C doesn’t support namespaces, which is why the
Cocoa functions require prefixes like NS
, CA
,
AV
, etc to avoid naming collisions. This applies to classes, too.
The recommended convention is to use a three-letter prefix for your
application-specific classes (e.g., XYZCar
).
While this is pretty much everything you need to know to start writing your
own classes, we did skim over some important details, so don’t worry if
you’re feeling not entirely comfortable with properties or methods. The
next module will begin filling in these holes with a closer look at the
@property
directive and all of the attributes that affect its
behavior.
Be sure to check out Ry’s Cocoa Tutorial. This brand new guide is a complete walkthrough of Mac App development, and it leverages all of the Objective-C skills that we just discussed. Learn more › |
Mailing List
Sign up for my low-volume mailing list to find out when new content is released. Next up is a comprehensive Swift tutorial planned for late January.
You’ll only receive emails when new tutorials are released, and your contact information will never be shared with third parties. Click here to unsubscribe.