| name | lang-objc-library-dev |
| description | Objective-C-specific library and framework development patterns. Use when creating reusable frameworks, designing public APIs with protocols and categories, configuring Podspecs/Cartfiles/Package.swift, managing header visibility, implementing XCTest suites, writing HeaderDoc/appledoc documentation, or publishing to CocoaPods/Carthage/SPM. Extends lang-objc-dev with framework tooling and distribution practices. |
Objective-C Library Development
Objective-C-specific patterns for library and framework development. This skill extends lang-objc-dev with framework tooling, API design patterns, and ecosystem practices for creating reusable components.
This Skill Extends
lang-objc-dev - Foundational Objective-C patterns (classes, protocols, categories, memory management)
For general Objective-C syntax, memory management, and Foundation framework usage, see the foundational skill first.
This Skill Adds
- Framework tooling: Xcode framework projects, umbrella headers, module maps
- API design: Public/private header organization, protocol-driven design, category patterns
- Testing: XCTest for libraries, dependency injection, mocking strategies
- Documentation: HeaderDoc, appledoc, inline documentation
- Distribution: CocoaPods, Carthage, Swift Package Manager integration
This Skill Does NOT Cover
- General Objective-C patterns - see
lang-objc-dev
- Swift interoperability - see
lang-swift-objc-bridge-dev
- iOS/macOS application development - see platform-specific skills
- Advanced design patterns - see
lang-objc-patterns-dev
Quick Reference
| Task | Command/Tool |
|---|
| New framework | Xcode: File > New > Project > Framework |
| Build framework | xcodebuild -scheme MyFramework build |
| Run tests | xcodebuild test -scheme MyFramework |
| CocoaPods spec | pod spec create MyLibrary |
| Validate podspec | pod lib lint MyLibrary.podspec |
| Publish to CocoaPods | pod trunk push MyLibrary.podspec |
| Carthage build | carthage build --no-skip-current |
| SPM build | swift build |
| Generate docs | appledoc --project-name MyLib *.h |
Framework Project Structure
Standard Framework Layout
MyLibrary.framework/
├── MyLibrary.xcodeproj
├── MyLibrary/
│ ├── MyLibrary.h # Umbrella header
│ ├── Info.plist
│ ├── Public/ # Public headers
│ │ ├── MLPublicClass.h
│ │ ├── MLProtocol.h
│ │ └── MLConstants.h
│ ├── Private/ # Private headers (not exported)
│ │ └── MLInternal.h
│ └── Implementation/ # .m files
│ ├── MLPublicClass.m
│ └── MLPrivateHelper.m
├── MyLibraryTests/
│ └── MyLibraryTests.m
├── MyLibrary.podspec
├── Cartfile
├── Package.swift
├── README.md
├── CHANGELOG.md
└── LICENSE
Umbrella Header
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT double MyLibraryVersionNumber;
FOUNDATION_EXPORT const unsigned char MyLibraryVersionString[];
#import <MyLibrary/MLPublicClass.h>
#import <MyLibrary/MLProtocol.h>
#import <MyLibrary/MLConstants.h>
#import <MyLibrary/MLCategories.h>
Header Visibility Configuration
In Xcode target settings:
Build Phases > Headers
├── Public # Exported headers (API)
│ ├── MyLibrary.h
│ ├── MLPublicClass.h
│ └── MLProtocol.h
├── Private # Not exported, but visible to framework
│ └── MLInternal.h
└── Project # Completely internal
└── MLPrivateHelper.h
Public API Design
Header Organization Patterns
Pattern 1: Monolithic Umbrella
#import <MyLibrary/MLClassA.h>
#import <MyLibrary/MLClassB.h>
#import <MyLibrary/MLClassC.h>
Pattern 2: Feature-Based Modules
#import <MyLibrary/MLCore.h> // Core functionality
#import <MyLibrary/MLNetworking.h> // Network-related
#import <MyLibrary/MLUI.h> // UI components
#import <MyLibrary/MLCoreClass.h>
#import <MyLibrary/MLCoreProtocol.h>
Pattern 3: Prefix-Based Organization
ML = MyLibrary
MLC = MyLibrary Core
MLN = MyLibrary Networking
MLU = MyLibrary UI
@interface MLCClient : NSObject
@interface MLNRequest : NSObject
@interface MLUButton : UIButton
Protocol-Driven API Design
Define clear protocols for extensibility:
@protocol MLDataProvider <NSObject>
@required
- (void)fetchDataWithCompletion:(void (^)(NSData *_Nullable data, NSError *_Nullable error))completion;
@optional
- (void)cancelFetch;
- (void)configureWithOptions:(NSDictionary<NSString *, id> *)options;
@end
Provide default implementation:
@interface MLDefaultDataProvider : NSObject <MLDataProvider>
- (instancetype)initWithBaseURL:(NSURL *)baseURL NS_DESIGNATED_INITIALIZER;
+ (instancetype)providerWithBaseURL:(NSURL *)baseURL;
@end
@implementation MLDefaultDataProvider
- (instancetype)initWithBaseURL:(NSURL *)baseURL {
self = [super init];
if (self) {
_baseURL = [baseURL copy];
}
return self;
}
+ (instancetype)providerWithBaseURL:(NSURL *)baseURL {
return [[self alloc] initWithBaseURL:baseURL];
}
- (void)fetchDataWithCompletion:(void (^)(NSData *, NSError *))completion {
}
@end
Category-Based API Extensions
Provide convenience methods via categories:
@interface NSString (MLAdditions)
- (NSString *)ml_md5Hash;
- (NSString *)ml_urlEncode;
- (nullable NSString *)ml_substringWithRange:(NSRange)range;
@end
@implementation NSString (MLAdditions)
- (NSString *)ml_md5Hash {
const char *cStr = [self UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x", digest[i]];
}
return output;
}
- (NSString *)ml_urlEncode {
NSCharacterSet *allowed = [NSCharacterSet URLQueryAllowedCharacterSet];
return [self stringByAddingPercentEncodingWithAllowedCharacters:allowed];
}
- (nullable NSString *)ml_substringWithRange:(NSRange)range {
if (range.location + range.length > self.length) {
;
}
[ substringWithRange:range];
}
Naming convention for categories:
- Prefix all methods with library prefix (e.g.,
ml_)
- Prevents collisions with other libraries or Apple's private APIs
- Makes it clear the method is from your library
Constants and Enumerations
Define constants in a dedicated header:
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT NSNotificationName const MLDidCompleteNotification;
FOUNDATION_EXPORT NSNotificationName const MLDidFailNotification;
FOUNDATION_EXPORT NSErrorDomain const MLErrorDomain;
typedef NS_ERROR_ENUM(MLErrorDomain, MLErrorCode) {
MLErrorCodeUnknown = 0,
MLErrorCodeInvalidParameter = 1,
MLErrorCodeNetworkFailure = 2,
MLErrorCodeAuthenticationRequired = 3,
};
FOUNDATION_EXPORT NSString * const MLConfigurationKeyTimeout;
FOUNDATION_EXPORT NSString * const MLConfigurationKeyRetryCount;
typedef NS_OPTIONS(NSUInteger, MLOptions) {
MLOptionNone = 0,
MLOptionVerbose = 1 << 0,
MLOptionAsync = 1 << 1,
MLOptionPersistent = 1 << 2,
};
NSNotificationName const MLDidCompleteNotification = @"MLDidCompleteNotification";
NSNotificationName const MLDidFailNotification = @"MLDidFailNotification";
NSErrorDomain const MLErrorDomain = @"com.example.mylibrary.error";
* MLConfigurationKeyTimeout = ;
* MLConfigurationKeyRetryCount = ;
Version Information
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT NSString * const MLVersionString;
FOUNDATION_EXPORT NSInteger const MLVersionMajor;
FOUNDATION_EXPORT NSInteger const MLVersionMinor;
FOUNDATION_EXPORT NSInteger const MLVersionPatch;
NSString * const MLVersionString = @"1.2.3";
NSInteger const MLVersionMajor = 1;
NSInteger const MLVersionMinor = 2;
NSInteger const MLVersionPatch = 3;
XCTest Testing Strategies
Test Target Organization
MyLibraryTests/
├── Unit/
│ ├── MLClassATests.m
│ ├── MLClassBTests.m
│ └── MLUtilsTests.m
├── Integration/
│ ├── MLNetworkingTests.m
│ └── MLDataFlowTests.m
├── Helpers/
│ ├── MLTestHelpers.h
│ ├── MLTestHelpers.m
│ └── MLMockObjects.m
└── Resources/
├── test_data.json
└── fixture.plist
Unit Test Patterns
#import <XCTest/XCTest.h>
#import <MyLibrary/MyLibrary.h>
@interface MLPublicClassTests : XCTestCase
@property (nonatomic, strong) MLPublicClass *sut;
@end
@implementation MLPublicClassTests
- (void)setUp {
[super setUp];
self.sut = [[MLPublicClass alloc] init];
}
- (void)tearDown {
self.sut = nil;
[super tearDown];
}
#pragma mark - Initialization Tests
- (void)testDesignatedInitializer {
MLPublicClass *instance = [[MLPublicClass alloc] initWithName:@"Test"];
XCTAssertNotNil(instance, @"Instance should not be nil");
XCTAssertEqualObjects(instance.name, @"Test", @"Name should be set");
}
- (void)testConvenienceInitializer {
MLPublicClass *instance = [MLPublicClass instanceWithName:@"Test"];
XCTAssertNotNil(instance);
XCTAssertEqualObjects(instance.name, @"Test");
}
#pragma mark - Behavior Tests
- (void)testProcessDataWithValidInput {
NSData *input = [@"valid data" dataUsingEncoding:];
*error = ;
result = [.sut processData:input error:&error];
(result, );
(error, );
}
- ()testProcessDataWithInvalidInput {
*error = ;
result = [.sut processData: error:&error];
(result, );
(error, );
(error.code, MLErrorCodeInvalidParameter);
}
- ()testAsyncFetchWithExpectation {
*expectation = [ expectationWithDescription:];
[.sut fetchDataWithCompletion:^( *data, *error) {
(data, );
(error, );
[expectation fulfill];
}];
[ waitForExpectationsWithTimeout: handler:^( *error) {
(error) {
(, error);
}
}];
}
- ()testEmptyString {
*result = [.sut processString:];
(result, , );
}
- ()testNilParameter {
([.sut processString:], );
}
- ()testLargeInput {
*large = [ string];
( i = ; i < ; i++) {
[large appendString:];
}
*result = [.sut processString:large];
(result, );
}
Mock Objects for Testing
@interface MLMockDataProvider : NSObject <MLDataProvider>
@property (nonatomic, strong) NSData *mockData;
@property (nonatomic, strong) NSError *mockError;
@property (nonatomic, assign) BOOL shouldFail;
@property (nonatomic, assign) NSTimeInterval delay;
@end
@implementation MLMockDataProvider
- (void)fetchDataWithCompletion:(void (^)(NSData *, NSError *))completion {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delay * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
if (self.shouldFail) {
completion(nil, self.mockError);
} else {
completion(self.mockData, nil);
}
});
}
@end
- (void)testWithMockProvider {
MLMockDataProvider *mock = [[MLMockDataProvider alloc] init];
mock.mockData = [@"test" dataUsingEncoding:NSUTF8StringEncoding];
mock.shouldFail = NO;
MLClient *client = [[MLClient alloc] initWithDataProvider:mock];
}
Test Helpers
@interface MLTestHelpers : NSObject
+ (NSDictionary *)loadJSONFixture:(NSString *)filename;
+ (NSURL *)createTemporaryDirectory;
+ (void)cleanupTemporaryDirectory:(NSURL *)directory;
@end
@implementation MLTestHelpers
+ (NSDictionary *)loadJSONFixture:(NSString *)filename {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSURL *url = [bundle URLForResource:filename withExtension:@"json"];
NSData *data = [NSData dataWithContentsOfURL:url];
return [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
}
+ (NSURL *)createTemporaryDirectory {
NSString *tempDir = NSTemporaryDirectory();
NSString *guid = [[NSUUID UUID] UUIDString];
NSString *path = [tempDir stringByAppendingPathComponent:guid];
[[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:nil];
[ fileURLWithPath:path];
}
+ ()cleanupTemporaryDirectory:( *)directory {
[[ defaultManager] removeItemAtURL:directory error:];
}
HeaderDoc and Documentation
HeaderDoc Syntax
@interface MLPublicClass : NSObject
@property (nonatomic, copy, readonly) NSString *name;
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
+ (instancetype)instanceWithName:(NSString *)name;
- (BOOL)processData:(NSData *)data error:( **)error;
- ()fetchDataWithCompletion:( (^)( *_Nullable data, *_Nullable error))completion;
Nullability Annotations
NS_ASSUME_NONNULL_BEGIN
@interface MLClient : NSObject
- (NSString *)getName;
- (nullable NSString *)getOptionalValue;
- (void)setName:(NSString *)name;
- (void)setOptionalValue:(nullable NSString *)value;
- (void)fetchWithCompletion:(void (^)(NSData *_Nullable data, NSError *_Nullable error))completion;
@end
NS_ASSUME_NONNULL_END
Appledoc Configuration
Create a .appledoc configuration file:
--project-name "MyLibrary"
--project-company "Your Company"
--company-id "com.yourcompany"
--output "./Documentation"
--logformat xcode
--keep-intermediate-files
--no-repeat-first-par
--no-warn-invalid-crossref
--ignore "*.m"
--ignore "Private"
--index-desc "./README.md"
Generate documentation:
appledoc .
CocoaPods Distribution
Podspec Structure
Pod::Spec.new do |s|
s.name = 'MyLibrary'
s.version = '1.2.3'
s.summary = 'A brief description of MyLibrary'
s.description = <<-DESC
A longer description of MyLibrary that explains what it does,
why it exists, and what problems it solves. This should be
more detailed than the summary.
DESC
s.homepage = 'https://github.com/yourusername/mylibrary'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Your Name' => 'you@example.com' }
s.source = { :git => 'https://github.com/yourusername/mylibrary.git',
:tag => s.version.to_s }
s.ios.deployment_target = '12.0'
s.osx.deployment_target = '10.13'
s.watchos.deployment_target = '5.0'
s.tvos.deployment_target = '12.0'
s.source_files = 'MyLibrary/**/*.{h,m}'
s.public_header_files = 'MyLibrary/Public/**/*.h'
s.private_header_files = 'MyLibrary/Private/**/*.h'
s.resources = 'MyLibrary/Resources/**/*'
s.dependency 'AFNetworking', '~> 4.0'
s.ios.dependency 'SDWebImage', '~> 5.0'
s.frameworks = 'Foundation', 'UIKit'
s.requires_arc =
s.pod_target_xcconfig = {
=>
}
s.subspec ||
core.source_files =
core.public_header_files =
s.subspec ||
net.source_files =
net.dependency
net.dependency ,
s.default_subspecs =
CocoaPods Workflow
pod spec create MyLibrary
pod lib lint MyLibrary.podspec
pod spec lint MyLibrary.podspec
pod trunk register you@example.com 'Your Name'
pod trunk push MyLibrary.podspec
pod trunk add-owner MyLibrary other@example.com
Podfile for Development
platform :ios, '12.0'
use_frameworks!
target 'MyLibrary' do
pod 'AFNetworking', '~> 4.0'
end
target 'MyLibraryTests' do
pod 'OCMock', '~> 3.9'
end
Carthage Distribution
Cartfile Structure
# Cartfile - Dependencies for your library
github "AFNetworking/AFNetworking" ~> 4.0
Carthage Compatibility
Ensure your framework is Carthage-compatible:
- Use shared schemes - Make sure your framework scheme is shared
- Build universal frameworks - Support both iOS devices and simulator
carthage build --no-skip-current
carthage build --use-xcframeworks
Build Script for Universal Framework
#!/bin/bash
FRAMEWORK_NAME="MyLibrary"
SCHEME="MyLibrary"
xcodebuild archive \
-scheme "${SCHEME}" \
-archivePath "build/ios.xcarchive" \
-sdk iphoneos \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
xcodebuild archive \
-scheme "${SCHEME}" \
-archivePath "build/ios-simulator.xcarchive" \
-sdk iphonesimulator \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
xcodebuild -create-xcframework \
-framework "build/ios.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \
-framework "build/ios-simulator.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \
-output "build/${FRAMEWORK_NAME}.xcframework"
Swift Package Manager
Package.swift
import PackageDescription
let package = Package(
name: "MyLibrary",
platforms: [
.iOS(.v12),
.macOS(.v10_13),
.watchOS(.v5),
.tvOS(.v12)
],
products: [
.library(
name: "MyLibrary",
targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/AFNetworking/AFNetworking.git", from: "4.0.0"),
],
targets: [
.target(
name: "MyLibrary",
dependencies: ["AFNetworking"],
path: "MyLibrary",
publicHeadersPath: "Public",
cSettings: [
.headerSearchPath("Private"),
]),
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"],
path: "MyLibraryTests"),
]
)
SPM Directory Structure
MyLibrary/
├── Package.swift
├── Sources/
│ └── MyLibrary/
│ ├── include/ # Public headers
│ │ ├── MyLibrary.h
│ │ ├── MLPublicClass.h
│ │ └── MLProtocol.h
│ └── Implementation/ # .m files
│ └── MLPublicClass.m
├── Tests/
│ └── MyLibraryTests/
│ └── MyLibraryTests.m
└── README.md
Module Maps
Custom Module Map
// module.modulemap
framework module MyLibrary {
umbrella header "MyLibrary.h"
export *
module * { export * }
explicit module Private {
header "MLInternal.h"
export *
}
}
Module Map in Xcode
Configure in Build Settings:
DEFINES_MODULE = YES
MODULEMAP_FILE = $(SRCROOT)/MyLibrary/module.modulemap
Versioning and Release Strategy
Semantic Versioning
Follow semver.org:
- MAJOR: Incompatible API changes
- MINOR: Backwards-compatible functionality additions
- PATCH: Backwards-compatible bug fixes
Deprecation Strategy
- (void)oldMethod:(id)parameter
DEPRECATED_MSG_ATTRIBUTE("Use newMethod:withOptions: instead");
- (void)newMethod:(id)parameter
withOptions:(NSDictionary *)options;
- (void)oldMethod:(id)parameter {
NSLog(@"Warning: oldMethod: is deprecated. Use newMethod:withOptions:");
[self newMethod:parameter withOptions:@{}];
}
Version Macro
#define ML_VERSION_MAJOR 1
#define ML_VERSION_MINOR 2
#define ML_VERSION_PATCH 3
#define ML_VERSION_CHECK(major, minor, patch) \
(ML_VERSION_MAJOR > (major) || \
(ML_VERSION_MAJOR == (major) && ML_VERSION_MINOR > (minor)) || \
(ML_VERSION_MAJOR == (major) && ML_VERSION_MINOR == (minor) && ML_VERSION_PATCH >= (patch)))
#if ML_VERSION_CHECK(1, 2, 0)
#endif
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New feature in development
## [1.2.3] - 2024-01-15
### Fixed
- Fixed crash when processing nil data
- Corrected memory leak in network layer
## [1.2.0] - 2024-01-01
### Added
- New `MLClient` class for network operations
- Support for custom data providers via `MLDataProvider` protocol
### Changed
- Improved performance of data processing by 50%
- Updated minimum deployment target to iOS 12.0
### Deprecated
- `oldMethod:` in favor of `newMethod:withOptions:`
## [1.1.0] - 2023-12-01
### Added
- Initial public release
Troubleshooting
Framework Not Found at Runtime
Symptoms: dyld: Library not loaded: @rpath/MyLibrary.framework/MyLibrary
Solution:
1. Check Build Settings > Runpath Search Paths
- Should include @executable_path/Frameworks
2. Verify framework is embedded:
- Build Phases > Embed Frameworks
- Framework should be listed
3. For CocoaPods, ensure:
- use_frameworks! is in Podfile
- Run pod install after changes
Duplicate Symbol Errors
Symptoms: duplicate symbol _OBJC_CLASS_$_ClassName
Solution:
@implementation NSString (Additions)
- (NSString *)reverse { ... }
@end
@implementation NSString (MLAdditions)
- (NSString *)ml_reverse { ... }
@end
Missing Header in Umbrella
Symptoms: Header not visible to framework users
Solution:
1. Check header visibility in Xcode:
- Target > Build Phases > Headers
- Move header from Project/Private to Public
2. Add to umbrella header:
- #import <MyLibrary/MLNewClass.h>
3. Clean and rebuild:
- Product > Clean Build Folder (Cmd+Shift+K)
Swift Bridging Issues
Symptoms: Objective-C types not visible in Swift
Solution:
NS_ASSUME_NONNULL_BEGIN
@interface MLClass : NSObject
- (NSString *)getName;
- (nullable NSString *)getOptionalName;
@end
NS_ASSUME_NONNULL_END
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic, copy) NSDictionary<NSString *, NSNumber *> *counts;
Podspec Validation Failures
Symptoms: pod lib lint fails
Solution:
pod lib lint --verbose
Best Practices Summary
- Prefix all public symbols with 2-3 letter prefix (classes, protocols, constants, categories)
- Use nullability annotations for Swift interoperability
- Document with HeaderDoc for generated documentation
- Provide protocol-based APIs for extensibility
- Include comprehensive tests with XCTest
- Support multiple distribution methods (CocoaPods, Carthage, SPM)
- Follow semantic versioning strictly
- Maintain backwards compatibility when possible
- Deprecate before removing APIs
- Keep umbrella header clean and organized
See Also