If you’re new to Objective C and iOS development, this topic will explain some important things about Forward Declaration and will help you understand the concept behind it.So, let’s skip the theory and jump right into a small example to see what it’s all about.We’ll create a small project, that is quite useless, but will at the same time serve as a good example and
cmake
utility.So, here is…Empty class
Warrior
// Warrior.h
#import <Foundation/Foundation.h>
@interface Warrior : NSObject
@end
// Warrior.m
#import "Warrior.h"
@implementation Warrior
@end
Empty class
Armor
// Armor.h
#import <Foundation/Foundation.h>
@interface Armor : NSObject
@end
// Armor.m
#import "Armor.h"
@implementation Armor
@end
main.m
as a driver – it just creates
Warrior
object and prints it
// main.m
#import <Foundation/Foundation.h>
#import "Warrior.h"
int main() {
Warrior *w = [Warrior new];
NSLog(@"%@", w);
return 0;
}
And
cmake
configuration file
// CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -framework Foundation")
set(SOURCES Warrior.m Armor.m main.m)
add_executable(fdecl ${SOURCES})
So, let’s build our simple program
$ cd directory_with_project
$ cmake . # prepare makefile
$ make # build
We see which modules are being built
Scanning dependencies of target fdecl
[ 33%] Building CXX object CMakeFiles/fdecl.dir/Warrior.m.o
[ 66%] Building CXX object CMakeFiles/fdecl.dir/Armor.m.o
[100%] Building CXX object CMakeFiles/fdecl.dir/main.m.o
Linking CXX executable fdecl
[100%] Built target fdecl
And now we’ll add an ability to wear armor
// Warrior.h
@interface Warrior : NSObject
- (void)wearArmor:(Armor *)armor;
@end
// Warrior.m
#import "Warrior.h"
@implementation Warrior
{
Armor *_armor;
}
- (void)wearArmor:(Armor *)armor
{
_armor = armor;
}
@end
Remember this state, we’ll get back to it later.We need to inform compiler about the
Armor
class in order to compile these changes.The first approach is to use
#include/#import
statements and it’s being used by a lot of developers.So let’s do it
// Warrior.h
#import "Armor.h"
@interface Warrior : NSObject
- (void)wearArmor:(Armor *)armor;
@end
and build
$ make
Scanning dependencies of target fdecl
[ 33%] Building CXX object CMakeFiles/fdecl.dir/Warrior.m.o
[ 66%] Building CXX object CMakeFiles/fdecl.dir/main.m.o
Linking CXX executable fdecl
[100%] Built target fdecl
Looks good, but let’s add some property to our instance of
Armor
class, e.g. ‘is it magical?’
// Armor.h
#import <Foundation/Foundation.h>
@interface Armor : NSObject
- (BOOL)isMagical;
@end
// Armor.m
#import "Armor.h"
@implementation Armor
- (BOOL)isMagical
{
return YES;
}
@end
and let’s build our program again
$ make
Scanning dependencies of target fdecl
[ 33%] Building CXX object CMakeFiles/fdecl.dir/Warrior.m.o
[ 66%] Building CXX object CMakeFiles/fdecl.dir/Armor.m.o
[100%] Building CXX object CMakeFiles/fdecl.dir/main.m.o
Linking CXX executable fdecl
[100%] Built target fdecl
Good news is that the build of
main.m
was successful. There’s one thing though. Despite of the fact that the build has been successful, workflow has not been optimal – main doesn’t use
Armor
, therefore, it must know nothing about this class.Ok, let’s get back to the state where we’ve added
wearArmor:
method, but haven’t included
Armor
yet.Now we’ll go another way:Use
#import
at implementation (
Warrior.m
) and forward declaration (
@class Armor;
) at header (
Warrior.h
)
// Warrior.h
@class Armor;
@interface Warrior : NSObject
- (void)wearArmor:(Armor *)armor;
@end
// Warrior.m
#import "Armor.h"
@implementation Warrior
{
Armor *_armor;
}
- (void)wearArmor:(Armor *)armor
{
_armor = armor;
}
@end
Make it (make sure that you’ve removed
isMagical
from
Armor
, we need this to get the same experience as before)
$ make
Add
isMagical
method again
// Armor.h
#import <Foundation/Foundation.h>
@interface Armor : NSObject
- (BOOL)isMagical;
@end
// Armor.m
#import "Armor.h"
@implementation Armor
- (BOOL)isMagical
{
return YES;
}
@end
Build and check the results
$ make
Scanning dependencies of target fdecl
[ 33%] Building CXX object CMakeFiles/fdecl.dir/Warrior.m.o
[ 66%] Building CXX object CMakeFiles/fdecl.dir/Armor.m.o
Linking CXX executable fdecl
[100%] Built target fdecl
Now it looks much better. Did you notice an important difference?In our example we have only three modules, so the benefit is barely noticeable.Things change dramatically when you’re working on a large project consisting of numerous classes and modules with lots of dependencies and complex relationships. Every time you add some small thing into the project you’ll have to wait more and more for compiler to re-build all the things. This is where you can get the full benefit from utilising FD approach.You can experience exactly the same problems when using
@protocol
s as delegates.
// Awesome.h
@class Awesome;
@protocol AwesomeDelegate
- (void)awesome:(Awesome *)awesome didFailed:(NSError *)error;
@end
@interface Awesome : NSObject
@property (weak, nonatomic) id delegate;
@end
// and then include this header into each class that want to be an awesome delegate
So the right way is to use different files for a delegate and a class, which use this a delegate:
// AwesomeDelegate.h
@class Awesome;
@protocol AwesomeDelegate
- (void)awesome:(Awesome *)awesome didFailed:(NSError *)error;
@end
// Awesome.h
@protocol AwesomeDelegate;
@interface Awesome : NSObject
@property (weak, nonatomic) id delegate;
@end
To summarise, keep in mind the importance of forward declaration, as it might save your time dramatically with every build you run for the project.In short, your goal is to create as clean interface as possible, that would be readable and maintainable by others in the future. Therefore, it must contain only imports of a base class and protocols, which current class conforms to. For all the rest there’s… no, not MasterCard, forward declaration in this case.