Modules, Frameworks and Code Organization
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Objective-C has no namespaces and no module system of its own — its unit of organisation is the header, and its unit of distribution is the framework. Clang modules, added later, fix the worst of the textual-include model without changing the language. Good organisation in Objective-C is therefore mostly a matter of header hygiene and naming discipline.
#import, #include and @import
| Directive | What it does |
|---|---|
|
C’s textual inclusion. Includes the file every time; needs include guards. |
|
The same, but never includes a file twice. The default for Objective-C headers. No guards needed. |
|
Loads a precompiled Clang module. Not textual at all — it imports a semantic description of the framework. |
#import <Foundation/Foundation.h> // a framework umbrella header
#import "MyClass.h" // a project header
#include <math.h> // a plain C header (#import also works)
@import Foundation; // the modular form
@import UIKit;
@import Foundation.NSString; // a submodule, if you want just a part
#import remains correct everywhere and is what most code still uses. Where modules are enabled (-fmodules,
on by default in Xcode), Clang silently converts a framework #import into a module import anyway, so the
practical difference is small.
The Header-Import Discipline
Two rules save more compile time and prevent more circular-dependency problems than anything else:
// In a HEADER: forward-declare whenever only the name is needed.
@class Customer;
@protocol Payable;
@interface Order : NSObject
@property (nonatomic, strong) Customer *customer;
@property (nonatomic, weak) id<Payable> payer;
@end
// In the IMPLEMENTATION: import the real headers.
#import "Order.h"
#import "Customer.h"
#import "Payable.h"
You must #import in the header only when you actually need the declaration there: inheriting from the class,
adopting the protocol, using an enum or typedef, or embedding a struct by value.
Clang Modules
A module is a precompiled, self-contained description of a library’s interface. It is parsed once and reused, rather than re-read as text in every translation unit — which is both dramatically faster and immune to the order-dependence and macro leakage of textual includes.
@import Foundation; // instead of #import <Foundation/Foundation.h>
@import CoreLocation;
Benefits worth knowing:
-
Build speed — the module is compiled once per configuration, not once per file.
-
Isolation — one header’s
#definecannot alter how the next one parses. -
Automatic linking — importing a module links its library, so no manual
-frameworkflag is needed.
module.modulemap
A module map tells Clang what a module contains. For your own framework:
framework module MyFramework {
umbrella header "MyFramework.h"
export *
module * { export * }
}
A module map for a plain (non-framework) library:
module MyLibrary {
umbrella header "MyLibrary.h"
export *
explicit module Private { // opt-in submodule
header "MyLibraryPrivate.h"
export *
}
}
Xcode generates a module map for a framework target automatically when DEFINES_MODULE = YES; you write one
by hand mainly to modularise a third-party C library.
Umbrella Headers
The umbrella header is a framework’s front door: it imports every public header, so one #import brings in
the whole API.
// MyFramework.h -- the umbrella header
#import <Foundation/Foundation.h>
//! Project version number for MyFramework.
FOUNDATION_EXPORT double MyFrameworkVersionNumber;
FOUNDATION_EXPORT const unsigned char MyFrameworkVersionString[];
#import <MyFramework/Person.h>
#import <MyFramework/Downloader.h>
#import <MyFramework/MyFrameworkTypes.h>
Clang warns if a public header is missing from the umbrella (umbrella header does not include header …) — that warning is worth treating as an error, because a header outside the umbrella is unreachable through the
module.
Frameworks
A framework is a bundle containing a library, its public headers, its module map and its resources.
| Static framework / library | Dynamic framework | |
|---|---|---|
Linked |
At build time, code copied into the binary |
At launch, loaded from the bundle |
App size |
Only the symbols actually used |
The whole framework |
Launch time |
No cost |
Small per-framework cost |
Shared between targets |
A separate copy in each |
One copy, shared (app + extension) |
Resources |
Not bundled (needs a separate resource bundle) |
Bundled with the code |
Objective-C categories |
Need |
Work normally |
Prefer dynamic frameworks when the code is shared between an app and its extensions, or when the framework carries resources. Prefer static when launch time matters more than binary size and the framework is used by exactly one target.
Anatomy of a Framework Bundle
the bundle directory"] F --> B["MyFramework
the binary: compiled code"] F --> H["Headers/"] F --> P["PrivateHeaders/"] F --> M["Modules/"] F --> R["Resources/"] F --> I["Info.plist
identifier, version, min OS"] H --> H1["MyFramework.h
umbrella header
imports every public header"] H --> H2["Person.h
Downloader.h
Public headers — your API surface"] P --> P1["Downloader+Internal.h
Private: reachable but unsupported"] M --> M1["module.modulemap
names the umbrella header,
enables @import MyFramework;"] M --> M2["MyFramework.swiftmodule
present if the target contains Swift"] R --> R1["Assets, .nib/.storyboard,
.strings, .bundle
dynamic frameworks only"] N["Project-visibility headers
(most of them)"] -.->|"not copied
into the bundle"| F style H1 fill:#dce9f7,stroke:#2f6fa8 style H2 fill:#dce9f7,stroke:#2f6fa8 style M1 fill:#dbe9d5,stroke:#4a7a3a style P1 fill:#f6e9cf,stroke:#b08a34 style N fill:#eeeeee,stroke:#999999
The three header roles in the next section map directly onto this layout: Public headers land in
Headers/ and must appear in the umbrella header, Private headers land in PrivateHeaders/, and Project
headers are not copied at all.
The -ObjC Flag
A classic failure: an app links a static library, calls a category method, and crashes with unrecognized
selector. The reason is that a category adds no new linker symbol, so the object file containing it looks
unused and is dropped.
# Force every Objective-C class and category from static libraries to be linked in.
-ObjC
# The bigger hammer (loads every object file, including non-Objective-C ones):
-all_load
-force_load /path/to/libMyLibrary.a
Add -ObjC to Other Linker Flags whenever you link a static library containing categories. See
Categories and Extensions.
Header Visibility
Xcode’s Build Phases ▸ Headers sorts each header into one of three roles:
| Role | Meaning |
|---|---|
Public |
Copied into the framework bundle and reachable by clients. Must be listed in the umbrella header. This is your API surface — changing it is a breaking change. |
Private |
Copied into the bundle’s |
Project |
Not copied at all. Visible only within the framework’s own compilation. The default, and the right choice for most headers. |
The corresponding language-level tools:
// Class extension in the .m -- entirely private (see Properties and Encapsulation)
@interface Downloader ()
@property (nonatomic, strong) NSURLSession *session;
@end
// A "+Internal" header for things other files in the framework need, kept Project-visibility
// Downloader+Internal.h
@interface Downloader (Internal)
- (void)beginNextChunk;
@end
Attribute-level visibility also exists for C symbols:
FOUNDATION_EXPORT NSString * const MyPublicConstant; // exported
__attribute__((visibility("hidden"))) void myInternalFunction(void);
Public API Hygiene
Two annotations belong on nearly every public class, because they convert conventions into compiler-checked rules:
NS_ASSUME_NONNULL_BEGIN
@interface Downloader : NSObject
/// The designated initializer. Subclasses must chain to this one.
- (instancetype)initWithSession:(NSURLSession *)session NS_DESIGNATED_INITIALIZER;
/// A Downloader cannot be created without a session.
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END
NS_DESIGNATED_INITIALIZER makes Clang verify that every subclass’s designated initializer chains to it and
that convenience initializers call self, not super. NS_UNAVAILABLE removes an inherited initializer
that would leave the object in an invalid state — a far better failure than a run-time assertion. See
Classes and Objects.
Round out a public header with NS_ASSUME_NONNULL_BEGIN/END, lightweight generics, API_AVAILABLE where
relevant, and doc comments — together these are what make the API import cleanly into Swift, per
Swift Interoperability.
@compatibility_alias
Gives an existing class a second name, which the compiler resolves transparently:
@compatibility_alias MYLegacyPerson Person;
// Old code continues to compile unchanged:
MYLegacyPerson *p = [[MYLegacyPerson alloc] init]; // really a Person
It is a renaming aid: alias the old name to the new class and existing call sites keep working while you
migrate. Note that it affects the compiler only — NSStringFromClass still reports the real name, and
NSClassFromString(@"MYLegacyPerson") returns Nil.
Organising a Class Across Files
A class too big for one file can be split with categories, each in its own pair:
MyViewController.h // the public interface
MyViewController.m // lifecycle and core behaviour
MyViewController+TableView.h/.m // UITableViewDataSource/Delegate conformance
MyViewController+Networking.h/.m // request handling
MyViewController+Internal.h // shared internals, Project visibility
Within a file, #pragma mark provides structure in Xcode’s jump bar at no cost:
#pragma mark - Lifecycle
- (instancetype)init { … }
- (void)dealloc { … }
#pragma mark - Public API
- (void)start { … }
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)s { … }
#pragma mark - Private
- (void)recomputeLayout { … }
The leading - draws a separator. A consistent ordering — lifecycle, public API, protocol conformances
grouped by protocol, then private helpers — makes any file in the project navigable the same way.
A note of caution: splitting a class across categories cannot move state, since categories cannot add ivars. If the split is mostly about state rather than behaviour, the class probably wants decomposing into collaborating objects instead. See Categories and Extensions.
Class Prefixes as Namespaces
Objective-C has one flat global namespace for class names, and two classes with the same name in one process is undefined behaviour — the runtime logs "Class X is implemented in both …" and picks one arbitrarily. Prefixes are the only defence.
| Scope | Convention |
|---|---|
Apple’s frameworks |
Two letters: |
Your library or framework |
Three or more letters, ideally distinctive: |
An application (not a library) |
A prefix is optional but still helpful. |
Category methods |
A lowercase prefix plus underscore on every method: |
C functions, constants, typedefs |
The same prefix: |
// A library's public surface, consistently prefixed:
@interface MYKDownloader : NSObject
@end
typedef NS_ENUM(NSInteger, MYKDownloadState) {
MYKDownloadStateIdle,
MYKDownloadStateRunning
};
extern NSErrorDomain const MYKErrorDomain;
extern NSTimeInterval MYKDefaultTimeout(void);
Swift has real modules, so a Swift class needs no prefix — and @objc(MYKAnalytics) is how you give a Swift
class an Objective-C-safe name when it is exposed. That is the one context in which prefixes are gradually
disappearing.
See Also
-
Categories and Extensions — splitting classes, and the
-ObjClinker issue. -
Swift Interoperability — umbrella headers and the generated header.
-
Coding Conventions and Style — naming in full.
-
Build and Tooling — the flags and dependency managers.