Convert Objective-C to Swift
Convert Objective-C code to idiomatic Swift. This skill extends meta-convert-dev with Objective-C-to-Swift specific type mappings, idiom translations, interoperability patterns, and tooling for gradual migration.
This Skill Extends
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Objective-C types → Swift types
- Idiom translations: Objective-C patterns → idiomatic Swift
- Error handling: NSError pattern → Swift throws/Result
- Memory management: Manual/ARC → Swift ARC with value semantics
- FFI/Interop: Bridging headers, @objc attributes, gradual migration strategies
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Objective-C language fundamentals - see
lang-objc-dev - Swift language fundamentals - see
lang-swift-dev - Reverse conversion (Swift → Objective-C) - see
convert-swift-objc - SwiftUI → UIKit migration - see platform-specific skills
Quick Reference
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| NSString * | String | Swift String is value type |
| NSInteger | Int | Platform-dependent signed integer |
| BOOL | Bool | Direct mapping |
| NSArray * | [Any] / [Element] | Generic array with type safety |
| NSMutableArray * | [Element] | Use var for mutability |
| NSDictionary * | [String: Any] / [Key: Value] | Generic dictionary |
| id | Any / AnyObject | Type-erased reference |
| id<Protocol> | any Protocol / some Protocol | Existential or opaque type |
| nullable id | Optional<Any> / Any? | Explicit optionality |
| instancetype | Self | Return type is class of receiver |
| typedef void (^Block)() | () -> Void | Closure type |
| @property (weak) | weak var | Weak reference |
| @property (copy) | let (for strings) | Immutable by default |
| [obj method:param] | obj.method(param) | Dot syntax |
When Converting Code
- Analyze source thoroughly - Understand memory management, protocols, categories
- Map types first - Create comprehensive type equivalence table
- Preserve semantics - Match behavior, not just syntax
- Adopt Swift idioms - Don't write "Objective-C code in Swift syntax"
- Handle edge cases - nil messaging, NSNull, dynamic typing
- Leverage type safety - Replace
idwith specific types or generics - Test equivalence - Same inputs → same outputs
Type System Mapping
Primitive Types
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| BOOL | Bool | Direct mapping (true/false vs YES/NO) |
| NSInteger | Int | Platform-dependent signed (32-bit or 64-bit) |
| NSUInteger | UInt | Platform-dependent unsigned |
| int | Int32 | Explicit 32-bit signed |
| unsigned int | UInt32 | Explicit 32-bit unsigned |
| long | Int (64-bit) / Int32 (32-bit) | Platform-dependent |
| long long | Int64 | Explicit 64-bit signed |
| float | Float | 32-bit floating point |
| double | Double | 64-bit floating point (default) |
| CGFloat | CGFloat | Platform-dependent float (bridged) |
| char | CChar / Int8 | C char type |
| unichar | UInt16 | Unicode character |
| void | Void / () | Unit type |
| instancetype | Self | Return type of current class |
Foundation Types
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| NSString * | String | Bridged; Swift String is value type |
| NSMutableString * | String (with var) | Swift strings mutable with var |
| NSNumber * | NSNumber / specific type | Bridge to Int, Double, Bool when possible |
| NSArray * | [Any] | Untyped array |
| NSArray<Type *> * | [Type] | Generic array with type safety |
| NSMutableArray * | [Element] (with var) | Mutable via var declaration |
| NSDictionary * | [String: Any] | Untyped dictionary |
| NSDictionary<K, V> * | [K: V] | Generic dictionary |
| NSMutableDictionary * | [K: V] (with var) | Mutable via var |
| NSSet * | Set<AnyHashable> | Untyped set |
| NSSet<Type *> * | Set<Type> | Generic set |
| NSData * | Data | Bridged value type |
| NSDate * | Date | Bridged value type |
| NSURL * | URL | Bridged value type |
| NSError * | Error / NSError | Conforming to Error protocol |
Nullability and Optionals
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| id | Any? | Implicitly optional in Objective-C |
| nullable id | Any? | Explicitly nullable |
| nonnull id | Any | Non-optional |
| _Nullable | ? | Optional |
| _Nonnull | Non-optional | Default in Swift |
| nil | nil | Same keyword, different semantics |
Blocks and Closures
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| void (^)(void) | () -> Void | No parameters, no return |
| NSInteger (^)(NSInteger) | (Int) -> Int | Single parameter |
| void (^)(NSString *, NSError *) | (String, Error) -> Void | Multiple parameters |
| typedef void (^Completion)() | typealias Completion = () -> Void | Type alias |
| @escaping (implicit) | @escaping (explicit) | Escaping closures marked in Swift |
Collection Types
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| NSArray<T *> * | [T] | Generic array |
| NSMutableArray<T *> * | var array: [T] | Mutable array |
| NSDictionary<K, V> * | [K: V] | Generic dictionary |
| NSMutableDictionary<K, V> * | var dict: [K: V] | Mutable dictionary |
| NSSet<T *> * | Set<T> | Generic set |
| NSMutableSet<T *> * | var set: Set<T> | Mutable set |
| NSOrderedSet<T *> * | Custom (no direct equivalent) | Use array with uniqueness logic |
Composite Types
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| @interface Class : Super | class Class: Super | Class declaration |
| @interface Class <Protocol> | class Class: Protocol | Protocol adoption |
| @interface Class () (extension) | private extension Class | Class extension |
| @interface Class (Category) | extension Class | Category → Extension |
| @protocol Protocol | protocol Protocol | Protocol definition |
| @property Type *prop | var prop: Type | Property |
| @property (readonly) | let prop | Immutable property |
| @property (weak) | weak var | Weak reference |
| @property (copy) | let / var | Copy semantic (Swift strings already copy) |
| enum TypeName | enum TypeName | Enumeration |
| NS_ENUM(NSInteger, Name) | enum Name: Int | Integer-backed enum |
| NS_OPTIONS(NSUInteger, Name) | struct Name: OptionSet | Option set |
| typedef | typealias | Type alias |
| struct | struct | Value type |
Idiom Translation
Pattern 1: Property Declaration and Access
Objective-C:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, weak) id<PersonDelegate> delegate;
@property (nonatomic, copy) NSString *bio;
@property (nonatomic, readonly) NSString *identifier;
@end
@implementation Person
@end
// Usage
Person *person = [[Person alloc] init];
person.name = @"Alice";
NSInteger age = person.age;
Swift:
class Person {
var name: String
var age: Int
weak var delegate: (any PersonDelegate)?
var bio: String // Swift strings already have value semantics
let identifier: String // readonly → let
init(name: String = "", age: Int = 0, identifier: String) {
self.name = name
self.age = age
self.identifier = identifier
self.bio = ""
}
}
// Usage
let person = Person(identifier: UUID().uuidString)
person.name = "Alice"
let age = person.age
Why this translation:
- Swift properties don't need
@propertydeclaration - Memory semantics:
strongis default,weakexplicit,copyunnecessary for value types - Initialization required for all non-optional stored properties
letfor immutable,varfor mutable
Pattern 2: Method Declaration and Invocation
Objective-C:
@interface Calculator : NSObject
- (NSInteger)add:(NSInteger)a to:(NSInteger)b;
+ (instancetype)sharedCalculator;
- (void)performCalculation:(NSInteger)input
completion:(void (^)(NSInteger result, NSError *error))completion;
@end
@implementation Calculator
- (NSInteger)add:(NSInteger)a to:(NSInteger)b {
return a + b;
}
+ (instancetype)sharedCalculator {
static Calculator *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc] init];
});
return shared;
}
@end
// Usage
Calculator *calc = [Calculator sharedCalculator];
NSInteger result = [calc add:5 to:10];
Swift:
class Calculator {
func add(_ a: Int, to b: Int) -> Int {
return a + b
}
static let shared = Calculator()
func performCalculation(
_ input: Int,
completion: @escaping (Result<Int, Error>) -> Void
) {
// Implementation
}
}
// Usage
let calc = Calculator.shared
let result = calc.add(5, to: 10)
Why this translation:
- Swift methods use parameter labels naturally
- Singleton:
static letis thread-safe and simpler than dispatch_once - Completion handlers: prefer
Result<T, E>over separate parameters @escapingmust be explicit for closures that outlive function scope
Pattern 3: Nullability and Optional Handling
Objective-C:
- (nullable NSString *)findUserName:(NSString *)userId {
User *user = [self.users objectForKey:userId];
return user ? user.name : nil;
}
// Usage
NSString *name = [self findUserName:@"123"];
if (name) {
NSLog(@"Name: %@", name);
} else {
NSLog(@"User not found");
}
// Nil messaging (safe)
NSString *upper = [name uppercaseString]; // Returns nil if name is nil
Swift:
func findUserName(_ userId: String) -> String? {
guard let user = users[userId] else {
return nil
}
return user.name
}
// Usage
if let name = findUserName("123") {
print("Name: \(name)")
} else {
print("User not found")
}
// Optional chaining
let upper = name?.uppercased() // nil if name is nil
// Nil coalescing
let displayName = findUserName("123") ?? "Unknown"
Why this translation:
- Swift optionals are explicit and type-safe
if let/guard letfor safe unwrapping- Optional chaining (
?.) replaces Objective-C nil messaging - Nil coalescing (
??) provides defaults - Force unwrap (
!) should be rare and justified
Pattern 4: Protocols and Delegation
Objective-C:
@protocol DataSourceDelegate <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (void)didSelectItemAtIndex:(NSInteger)index;
@end
@interface DataSource : NSObject
@property (nonatomic, weak) id<DataSourceDelegate> delegate;
@end
@implementation DataSource
- (void)loadData {
if ([self.delegate respondsToSelector:@selector(numberOfItems)]) {
NSInteger count = [self.delegate numberOfItems];
}
if ([self.delegate respondsToSelector:@selector(didSelectItemAtIndex:)]) {
[self.delegate didSelectItemAtIndex:0];
}
}
@end
Swift:
protocol DataSourceDelegate: AnyObject {
func numberOfItems() -> Int
func didSelectItem(at index: Int) // Optional methods not directly supported
}
// For optional protocol methods, use separate protocols or default implementations
extension DataSourceDelegate {
func didSelectItem(at index: Int) {
// Default implementation (makes it optional)
}
}
class DataSource {
weak var delegate: (any DataSourceDelegate)?
func loadData() {
guard let delegate = delegate else { return }
let count = delegate.numberOfItems()
delegate.didSelectItem(at: 0) // Safe to call due to default implementation
}
}
Why this translation:
- Swift protocols require
AnyObjectfor class-only protocols (enables weak references) - No
@required/@optional- use protocol extensions for default implementations - No runtime checks needed with type-safe protocols
any Protocolfor existential types (heterogeneous collections)
Pattern 5: Enumerations
Objective-C:
typedef NS_ENUM(NSInteger, HTTPStatus) {
HTTPStatusOK = 200,
HTTPStatusNotFound = 404,
HTTPStatusServerError = 500
};
typedef NS_OPTIONS(NSUInteger, FilePermissions) {
FilePermissionsRead = 1 << 0,
FilePermissionsWrite = 1 << 1,
FilePermissionsExecute = 1 << 2
};
// Usage
HTTPStatus status = HTTPStatusOK;
FilePermissions perms = FilePermissionsRead | FilePermissionsWrite;
Swift:
enum HTTPStatus: Int {
case ok = 200
case notFound = 404
case serverError = 500
}
struct FilePermissions: OptionSet {
let rawValue: UInt
static let read = FilePermissions(rawValue: 1 << 0)
static let write = FilePermissions(rawValue: 1 << 1)
static let execute = FilePermissions(rawValue: 1 << 2)
}
// Usage
let status = HTTPStatus.ok
let perms: FilePermissions = [.read, .write]
// Pattern matching
switch status {
case .ok:
print("Success")
case .notFound:
print("Not found")
case .serverError:
print("Server error")
}
Why this translation:
NS_ENUM→ Swiftenumwith raw valueNS_OPTIONS→OptionSetstruct for bitwise options- Swift enums are type-safe with pattern matching
- Array literal syntax for option sets
Pattern 6: Error Handling
Objective-C:
- (BOOL)loadDataFromFile:(NSString *)path error:(NSError **)error {
NSData *data = [NSData dataWithContentsOfFile:path options:0 error:error];
if (!data) {
return NO;
}
// Process data
return YES;
}
// Creating custom errors
- (BOOL)validateUser:(User *)user error:(NSError **)error {
if (user.name.length == 0) {
if (error) {
*error = [NSError errorWithDomain:@"com.example.validation"
code:100
userInfo:@{
NSLocalizedDescriptionKey: @"Name is required"
}];
}
return NO;
}
return YES;
}
// Usage
NSError *error = nil;
BOOL success = [self loadDataFromFile:@"data.txt" error:&error];
if (!success) {
NSLog(@"Error: %@", error.localizedDescription);
}
Swift:
enum ValidationError: Error {
case nameRequired
case invalidFormat(String)
}
func loadData(from path: String) throws -> Data {
return try Data(contentsOfFile: path)
}
func validateUser(_ user: User) throws {
guard !user.name.isEmpty else {
throw ValidationError.nameRequired
}
}
// Usage with do-catch
do {
let data = try loadData(from: "data.txt")
// Process data
} catch {
print("Error: \(error.localizedDescription)")
}
// Usage with try?
if let data = try? loadData(from: "data.txt") {
// Process data
}
// Usage with Result
func loadDataResult(from path: String) -> Result<Data, Error> {
Result { try Data(contentsOfFile: path) }
}
Why this translation:
- Swift uses exceptions (
throws) instead of error pointers - Custom error types conform to
Errorprotocol (usually enums) do-catchfor error handlingtry?for optional conversionResult<T, E>for explicit error values- More type-safe and composable than NSError pattern
Pattern 7: Categories → Extensions
Objective-C:
// NSString+Validation.h
@interface NSString (Validation)
- (BOOL)isValidEmail;
@end
// NSString+Validation.m
@implementation NSString (Validation)
- (BOOL)isValidEmail {
NSString *pattern = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
return [predicate evaluateWithObject:self];
}
@end
// Usage
NSString *email = @"user@example.com";
if ([email isValidEmail]) {
NSLog(@"Valid email");
}
Swift:
extension String {
var isValidEmail: Bool {
let pattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let predicate = NSPredicate(format: "SELF MATCHES %@", pattern)
return predicate.evaluate(with: self)
}
}
// Usage
let email = "user@example.com"
if email.isValidEmail {
print("Valid email")
}
Why this translation:
- Categories directly map to Swift extensions
- Prefer computed properties over methods when no parameters
- Extensions can add stored properties via associated objects (advanced)
- Swift extensions more powerful (can conform to protocols, add constraints)
Pattern 8: Blocks → Closures
Objective-C:
typedef void (^CompletionBlock)(NSData *data, NSError *error);
@interface NetworkManager : NSObject
- (void)fetchDataWithCompletion:(CompletionBlock)completion;
@end
@implementation NetworkManager
- (void)fetchDataWithCompletion:(CompletionBlock)completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [self loadData];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(data, nil);
}
});
});
}
// Avoiding retain cycles
- (void)setupCompletion {
__weak typeof(self) weakSelf = self;
self.completion = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
}
};
}
@end
Swift:
typealias CompletionBlock = (Result<Data, Error>) -> Void
class NetworkManager {
func fetchData(completion: @escaping CompletionBlock) {
DispatchQueue.global().async {
let data = self.loadData()
DispatchQueue.main.async {
completion(.success(data))
}
}
}
// Modern async/await (preferred)
func fetchData() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
fetchData { result in
continuation.resume(with: result)
}
}
}
// Avoiding retain cycles
var completion: (() -> Void)?
func setupCompletion() {
completion = { [weak self] in
guard let self = self else { return }
self.doSomething()
}
}
}
Why this translation:
- Closure types more concise than block types
@escapingrequired for stored closures- Prefer
Result<T, E>over separate data/error parameters - Swift async/await preferred over callbacks
[weak self]/guard let selfpattern cleaner than weak-strong dance
FFI & Interoperability (10th Pillar)
Objective-C to Swift interoperability is exceptional, enabling gradual migration - the primary migration strategy for large codebases. You can mix Objective-C and Swift in the same project, calling code bidirectionally.
Why FFI/Interop Matters for This Conversion
Unlike most language conversions, Objective-C → Swift supports:
- Incremental migration: Convert one class at a time while maintaining a working app
- Bidirectional calling: Swift can call Objective-C, Objective-C can call Swift
- Shared types: Foundation types bridge automatically
- Same runtime: Both use the Objective-C runtime on Apple platforms
Gradual Migration Strategy
┌─────────────────────────────────────────────────────────────┐
│ OBJECTIVE-C → SWIFT MIGRATION │
├─────────────────────────────────────────────────────────────┤
│ Phase 1: SETUP │
│ • Enable "Use Swift" in Xcode project │
│ • Create bridging header (auto-generated) │
│ • Import essential Objective-C headers │
├─────────────────────────────────────────────────────────────┤
│ Phase 2: IDENTIFY │
│ • Start with leaf classes (fewest dependencies) │
│ • Identify pure data models (easy to convert) │
│ • Map protocols and categories │
├─────────────────────────────────────────────────────────────┤
│ Phase 3: CONVERT │
│ • Convert one class at a time │
│ • Add @objc attribute to maintain Objective-C visibility │
│ • Test after each conversion │
├─────────────────────────────────────────────────────────────┤
│ Phase 4: MODERNIZE │
│ • Remove @objc where not needed │
│ • Adopt Swift-only features (optionals, value types) │
│ • Refactor to protocol-oriented design │
├─────────────────────────────────────────────────────────────┤
│ Phase 5: COMPLETE │
│ • Remove bridging header when all Objective-C gone │
│ • Pure Swift codebase │
└─────────────────────────────────────────────────────────────┘
Bridging Header
When you add the first Swift file to an Objective-C project, Xcode creates a bridging header.
ProjectName-Bridging-Header.h:
// Import Objective-C headers to expose them to Swift
#import "MyObjectiveCClass.h"
#import "DataModel.h"
#import "NetworkManager.h"
// Framework imports
#import <SomeFramework/SomeFramework.h>
Rules:
- Import Objective-C headers here to use them in Swift
- No need to import in individual Swift files
- Only for app targets (frameworks use module maps)
Using Objective-C from Swift
Objective-C class:
// Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)greet;
@end
// Person.m
@implementation Person
- (void)greet {
NSLog(@"Hello, I'm %@", self.name);
}
@end
Import in bridging header:
// ProjectName-Bridging-Header.h
#import "Person.h"
Use from Swift:
// No import needed - bridging header makes it available
let person = Person()
person.name = "Alice"
person.age = 30
person.greet()
Using Swift from Objective-C
Swift class with @objc:
// User.swift
@objc class User: NSObject {
@objc var username: String
@objc var email: String
@objc init(username: String, email: String) {
self.username = username
self.email = email
super.init()
}
@objc func validate() -> Bool {
return !username.isEmpty && !email.isEmpty
}
}
Use from Objective-C:
// Import generated header
#import "ProjectName-Swift.h"
// Use Swift class
User *user = [[User alloc] initWithUsername:@"alice" email:@"alice@example.com"];
BOOL isValid = [user validate];
Key points:
@objcattribute exposes Swift declarations to Objective-C- Swift class must inherit from NSObject (or @objc class)
- Xcode auto-generates
ProjectName-Swift.hheader - Not all Swift features available in Objective-C (generics, protocols with associated types, etc.)
@objc Attributes
| Attribute | Purpose | Example |
|-----------|---------|---------|
| @objc | Expose to Objective-C runtime | @objc func method() |
| @objc(name) | Custom Objective-C name | @objc(customName) func swiftName() |
| @objcMembers | Expose all members of class | @objcMembers class MyClass |
| @nonobjc | Hide from Objective-C | @nonobjc func swiftOnly() |
| @IBOutlet | Interface Builder outlet | @IBOutlet weak var label: UILabel! |
| @IBAction | Interface Builder action | @IBAction func buttonTapped() |
Type Bridging
Foundation types bridge automatically between Objective-C and Swift:
| Objective-C | Swift | Bridging |
|-------------|-------|----------|
| NSString | String | Automatic, toll-free |
| NSArray | [Any] | Automatic |
| NSDictionary | [AnyHashable: Any] | Automatic |
| NSSet | Set<AnyHashable> | Automatic |
| NSNumber | Int, Double, Bool | Automatic (context-dependent) |
| NSData | Data | Automatic |
| NSDate | Date | Automatic |
| NSURL | URL | Automatic |
| NSError | Error | Automatic (protocol conformance) |
Bridging is free - no performance cost for toll-free bridging.
Protocol Bridging
Objective-C protocol:
@protocol Drawable <NSObject>
- (void)draw;
@optional
- (void)setColor:(UIColor *)color;
@end
Use from Swift:
// Conforms to Objective-C protocol
class Circle: NSObject, Drawable {
func draw() {
print("Drawing circle")
}
func setColor(_ color: UIColor) {
// Optional method
}
}
Swift protocol exposed to Objective-C:
@objc protocol Vehicle {
@objc var speed: Double { get }
@objc func accelerate()
@objc optional func honk() // Optional methods need @objc protocol
}
Common Interop Patterns
Pattern: Mixed Inheritance
Objective-C base class:
@interface Animal : NSObject
@property (nonatomic, strong) NSString *name;
- (void)makeSound;
@end
Swift subclass:
class Dog: Animal {
var breed: String = ""
override func makeSound() {
print("\(name ?? "") barks!")
}
func wagTail() {
print("Wagging tail")
}
}
Pattern: Category on Swift Class
You can add Objective-C categories to Swift classes:
Swift class:
@objc class Calculator: NSObject {
@objc func add(_ a: Int, to b: Int) -> Int {
return a + b
}
}
Objective-C category:
@interface Calculator (Advanced)
- (NSInteger)multiply:(NSInteger)a by:(NSInteger)b;
@end
@implementation Calculator (Advanced)
- (NSInteger)multiply:(NSInteger)a by:(NSInteger)b {
return a * b;
}
@end
Pattern: Selector and Dynamic Dispatch
Objective-C dynamic behavior:
SEL selector = @selector(greet);
if ([person respondsToSelector:selector]) {
[person performSelector:selector];
}
Swift equivalent:
// Using @objc and Selector
@objc class Person: NSObject {
@objc func greet() {
print("Hello")
}
}
let selector = #selector(Person.greet)
if person.responds(to: selector) {
person.perform(selector)
}
// Modern Swift: prefer protocols and type safety
protocol Greeter {
func greet()
}
if let greeter = person as? Greeter {
greeter.greet()
}
Interop Limitations
Swift features NOT available in Objective-C:
| Swift Feature | Workaround |
|---------------|------------|
| Generics | Use specific types or id |
| Protocols with associated types | Use type-erased wrappers |
| Tuples | Use structs or separate parameters |
| Enums with associated values | Use separate classes or NS_ENUM |
| Value types (struct) | Use classes (NSObject subclass) |
| Optionals (except via _Nullable) | Use nullable annotations |
| Protocol extensions | Expose via concrete class |
Example: Type-erased wrapper for generic protocol:
// Swift generic protocol (not @objc compatible)
protocol Container {
associatedtype Element
func add(_ element: Element)
}
// Type-erased wrapper for Objective-C
@objc class AnyContainer: NSObject {
private let _add: (Any) -> Void
init<C: Container>(_ container: C) {
self._add = { element in
if let typedElement = element as? C.Element {
container.add(typedElement)
}
}
}
@objc func add(_ element: Any) {
_add(element)
}
}
Build Configuration
Mixed language targets:
-
Bridging header (Objective-C → Swift):
- Build Settings → "Objective-C Bridging Header"
- Path:
ProjectName/ProjectName-Bridging-Header.h
-
Generated interface (Swift → Objective-C):
- Automatic:
ProjectName-Swift.h - Import in Objective-C files
- Automatic:
-
Module name (for Swift imports):
- Build Settings → "Product Module Name"
- Default:
ProjectName
Testing Mixed Code
// XCTest works with both languages
class MixedTests: XCTestCase {
func testObjectiveCClass() {
let person = Person() // Objective-C class
person.name = "Alice"
XCTAssertEqual(person.name, "Alice")
}
func testSwiftClass() {
let user = User(username: "bob", email: "bob@example.com")
XCTAssertTrue(user.validate())
}
}
From Objective-C tests:
@import XCTest;
#import "ProjectName-Swift.h"
@interface MixedTests : XCTestCase
@end
@implementation MixedTests
- (void)testSwiftClass {
User *user = [[User alloc] initWithUsername:@"alice" email:@"alice@example.com"];
XCTAssertTrue([user validate]);
}
@end
Memory Management
Objective-C ARC → Swift ARC
Both languages use Automatic Reference Counting, but Swift adds value semantics.
| Objective-C | Swift | Notes |
|-------------|-------|-------|
| @property (strong) | var (default) | Strong reference |
| @property (weak) | weak var | Weak reference |
| @property (copy) | let / var | Swift Strings/Arrays already have value semantics |
| @property (assign) | var (value types) | Primitive types |
| __weak | weak | Weak in closures |
| __strong | Default | Strong in closures |
| __unsafe_unretained | unowned(unsafe) | Rarely used |
Reference Cycles
Objective-C:
@interface Parent : NSObject
@property (strong) Child *child;
@end
@interface Child : NSObject
@property (weak) Parent *parent; // Weak to break cycle
@end
// Block cycle
__weak typeof(self) weakSelf = self;
self.completion = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doWork];
}
};
Swift:
class Parent {
var child: Child?
}
class Child {
weak var parent: Parent? // Weak to break cycle
}
// Closure cycle
completion = { [weak self] in
guard let self = self else { return }
self.doWork()
}
// Unowned (when you know reference always exists)
completion = { [unowned self] in
self.doWork() // Crashes if self is deallocated
}
Value Semantics
Objective-C (reference types):
NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"A", @"B", nil];
NSMutableArray *array2 = array1; // Same object
[array2 addObject:@"C"];
// array1 also contains "C"
Swift (value types):
var array1 = ["A", "B"]
var array2 = array1 // Copy on write
array2.append("C")
// array1 still ["A", "B"], array2 is ["A", "B", "C"]
Why this matters:
- Swift structs, enums, and standard types (String, Array, Dictionary) are value types
- Copy-on-write optimization prevents unnecessary copying
- Reference types (classes) still work like Objective-C
- Prefer value types in Swift for simpler, safer code
Concurrency Patterns
GCD → DispatchQueue
Objective-C:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSData *data = [self heavyComputation];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateUIWithData:data];
});
});
Swift (GCD):
DispatchQueue.global().async {
let data = self.heavyComputation()
DispatchQueue.main.async {
self.updateUI(with: data)
}
}
Swift (modern async/await):
Task {
let data = await heavyComputation()
await MainActor.run {
updateUI(with: data)
}
}
// Async function
func heavyComputation() async -> Data {
// Computation
return data
}
Completion Handlers → Async/Await
Objective-C:
- (void)fetchUserWithId:(NSString *)userId
completion:(void (^)(User *user, NSError *error))completion {
[self.network GET:@"/users" parameters:@{@"id": userId} success:^(id response) {
User *user = [User fromJSON:response];
completion(user, nil);
} failure:^(NSError *error) {
completion(nil, error);
}];
}
Swift (callback style):
func fetchUser(
id: String,
completion: @escaping (Result<User, Error>) -> Void
) {
network.get("/users", parameters: ["id": id]) { result in
switch result {
case .success(let data):
let user = User(from: data)
completion(.success(user))
case .failure(let error):
completion(.failure(error))
}
}
}
Swift (async/await - preferred):
func fetchUser(id: String) async throws -> User {
let data = try await network.get("/users", parameters: ["id": id])
return User(from: data)
}
// Usage
Task {
do {
let user = try await fetchUser(id: "123")
print("Got user: \(user.name)")
} catch {
print("Error: \(error)")
}
}
Actors for Thread Safety
Objective-C (manual synchronization):
@interface BankAccount : NSObject
@property (atomic, assign) double balance;
@end
@implementation BankAccount
- (void)deposit:(double)amount {
@synchronized(self) {
self.balance += amount;
}
}
- (BOOL)withdraw:(double)amount {
@synchronized(self) {
if (self.balance >= amount) {
self.balance -= amount;
return YES;
}
return NO;
}
}
@end
Swift (actor - automatic synchronization):
actor BankAccount {
private var balance: Double = 0
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) -> Bool {
guard balance >= amount else {
return false
}
balance -= amount
return true
}
}
// Usage (automatically synchronized)
let account = BankAccount()
Task {
await account.deposit(amount: 100)
let success = await account.withdraw(amount: 50)
}
Common Pitfalls
1. Force Unwrapping Optionals
Problem: Crashes when value is nil
Objective-C:
// Nil messaging is safe
NSString *name = [user name]; // Returns nil if user is nil
NSString *upper = [name uppercaseString]; // Returns nil if name is nil
Swift (bad):
let name = user!.name // Crashes if user is nil
let upper = name!.uppercased() // Crashes if name is nil
Swift (good):
guard let user = user else { return }
let name = user.name
let upper = name?.uppercased() ?? ""
2. Forgetting @objc for Interop
Problem: Swift declarations not visible to Objective-C
// Bad: Not visible to Objective-C
class MyClass {
func myMethod() { }
}
// Good: Visible to Objective-C
@objc class MyClass: NSObject {
@objc func myMethod() { }
}
3. Using id Instead of Specific Types
Objective-C:
- (id)fetchData {
return @{@"key": @"value"};
}
Swift (avoid):
func fetchData() -> Any {
return ["key": "value"]
}
Swift (prefer):
func fetchData() -> [String: String] {
return ["key": "value"]
}
4. Ignoring Mutability Semantics
Objective-C:
NSArray *array = [NSArray arrayWithObjects:@"A", nil];
NSMutableArray *mutable = (NSMutableArray *)array; // Dangerous cast
[mutable addObject:@"B"]; // Runtime error
Swift:
let array = ["A"] // Immutable
// var mutable = array as! [String] // No such thing as "mutable cast"
var mutable = array // Copy
mutable.append("B") // Safe, modifies copy
5. Not Handling nil in Collections
Objective-C:
NSArray *items = @[@"A", [NSNull null], @"C"];
NSString *second = items[1]; // NSNull object
// [second uppercaseString]; // Crashes - NSNull doesn't respond
Swift:
// Use optionals
let items: [String?] = ["A", nil, "C"]
let second = items[1] // Optional<String>
let upper = second?.uppercased() // Safe
// Or filter nils
let nonNilItems = items.compactMap { $0 }
6. Misunderstanding Property Attributes
Objective-C:
@property (copy) NSMutableString *title; // Bad: copy makes it immutable
Swift:
// String already has value semantics
var title: String // No need for "copy"
// For reference types that should be copied
var items: [Item] {
didSet {
// Array automatically copies on mutation
}
}
7. Selector Typos
Objective-C:
[person performSelector:@selector(gret)]; // Typo: should be "greet"
// Runtime crash: unrecognized selector
Swift:
// Type-safe selector
#selector(Person.greet) // Compiler error if method doesn't exist
8. Bridging Overhead
Problem: Unnecessary bridging between types
Swift (inefficient):
let nsString: NSString = "Hello"
let swiftString = nsString as String // Bridge
let upper = swiftString.uppercased()
let nsUpper = upper as NSString // Bridge again
Swift (efficient):
let string = "Hello" // Swift String
let upper = string.uppercased()
// Use NSString only when required by API
Tooling
| Tool | Purpose | Notes |
|------|---------|-------|
| Xcode migration assistant | Automated Swift conversion | Use as starting point, manual refinement needed |
| swiftc | Swift compiler | Compiles Swift code |
| Swift REPL | Interactive Swift | swift command in terminal |
| swift-demangle | Demangle Swift symbols | Debug symbol names |
| SwiftLint | Swift style linter | Enforce Swift conventions |
| Objective-C → Swift converter (Xcode) | Edit → Convert → To Modern Objective-C Syntax first | Improves conversion quality |
Examples
Example 1: Simple - Data Model
Before (Objective-C):
// User.h
@interface User : NSObject
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithUsername:(NSString *)username email:(NSString *)email age:(NSInteger)age;
@end
// User.m
@implementation User
- (instancetype)initWithUsername:(NSString *)username email:(NSString *)email age:(NSInteger)age {
self = [super init];
if (self) {
_username = [username copy];
_email = [email copy];
_age = age;
}
return self;
}
@end
After (Swift):
struct User {
let username: String
let email: String
let age: Int
}
// Auto-generated memberwise initializer:
// init(username: String, email: String, age: Int)
// Usage
let user = User(username: "alice", email: "alice@example.com", age: 30)
Why this translation:
- Swift
structis simpler for pure data - No need for manual init - memberwise initializer is free
- Value semantics (copy) is default for structs
letfor immutable properties
Example 2: Medium - Protocol and Delegation
Before (Objective-C):
// DataSource.h
@protocol DataSourceDelegate <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (void)didSelectItemAtIndex:(NSInteger)index;
@end
@interface DataSource : NSObject
@property (nonatomic, weak) id<DataSourceDelegate> delegate;
- (void)loadData;
@end
// DataSource.m
@implementation DataSource
- (void)loadData {
if ([self.delegate respondsToSelector:@selector(numberOfItems)]) {
NSInteger count = [self.delegate numberOfItems];
NSLog(@"Loading %ld items", (long)count);
}
if ([self.delegate respondsToSelector:@selector(didSelectItemAtIndex:)]) {
[self.delegate didSelectItemAtIndex:0];
}
}
@end
// ViewController.m
@interface ViewController () <DataSourceDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
DataSource *dataSource = [[DataSource alloc] init];
dataSource.delegate = self;
[dataSource loadData];
}
- (NSInteger)numberOfItems {
return 10;
}
- (void)didSelectItemAtIndex:(NSInteger)index {
NSLog(@"Selected item at index %ld", (long)index);
}
@end
After (Swift):
protocol DataSourceDelegate: AnyObject {
func numberOfItems() -> Int
func didSelectItem(at index: Int)
}
// Default implementation makes method optional
extension DataSourceDelegate {
func didSelectItem(at index: Int) {
// Default: do nothing
}
}
class DataSource {
weak var delegate: (any DataSourceDelegate)?
func loadData() {
guard let delegate = delegate else { return }
let count = delegate.numberOfItems()
print("Loading \(count) items")
delegate.didSelectItem(at: 0)
}
}
class ViewController: UIViewController, DataSourceDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let dataSource = DataSource()
dataSource.delegate = self
dataSource.loadData()
}
func numberOfItems() -> Int {
return 10
}
func didSelectItem(at index: Int) {
print("Selected item at index \(index)")
}
}
Why this translation:
- Protocol extension provides default implementation (replaces @optional)
- No runtime checks needed - type system ensures conformance
any DataSourceDelegatefor existential type- More expressive parameter labels
Example 3: Complex - Network Manager with Callbacks
Before (Objective-C):
// NetworkManager.h
typedef void (^NetworkCompletion)(id response, NSError *error);
@interface NetworkManager : NSObject
+ (instancetype)sharedManager;
- (void)GET:(NSString *)path
parameters:(NSDictionary *)params
completion:(NetworkCompletion)completion;
@end
// NetworkManager.m
@implementation NetworkManager
+ (instancetype)sharedManager {
static NetworkManager *shared = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared = [[self alloc] init];
});
return shared;
}
- (void)GET:(NSString *)path
parameters:(NSDictionary *)params
completion:(NetworkCompletion)completion {
NSURL *url = [NSURL URLWithString:path];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
return;
}
NSError *parseError = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parseError) {
completion(nil, parseError);
} else {
completion(json, nil);
}
});
}];
[task resume];
}
@end
// Usage
[[NetworkManager sharedManager] GET:@"https://api.example.com/users"
parameters:@{@"page": @1}
completion:^(id response, NSError *error) {
if (error) {
NSLog(@"Error: %@", error.localizedDescription);
return;
}
NSArray *users = response[@"users"];
NSLog(@"Fetched %lu users", (unsigned long)users.count);
}];
After (Swift - callback style):
class NetworkManager {
static let shared = NetworkManager()
private init() {}
func get(
_ path: String,
parameters: [String: Any] = [:],
completion: @escaping (Result<Any, Error>) -> Void
) {
guard let url = URL(string: path) else {
completion(.failure(NetworkError.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(.failure(NetworkError.noData))
}
return
}
do {
let json = try JSONSerialization.jsonObject(with: data)
DispatchQueue.main.async {
completion(.success(json))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
task.resume()
}
}
enum NetworkError: Error {
case invalidURL
case noData
}
// Usage (callback style)
NetworkManager.shared.get("https://api.example.com/users", parameters: ["page": 1]) { result in
switch result {
case .success(let response):
if let dict = response as? [String: Any],
let users = dict["users"] as? [[String: Any]] {
print("Fetched \(users.count) users")
}
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
After (Swift - modern async/await):
class NetworkManager {
static let shared = NetworkManager()
private init() {}
func get(_ path: String, parameters: [String: Any] = [:]) async throws -> Any {
guard let url = URL(string: path) else {
throw NetworkError.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONSerialization.jsonObject(with: data)
}
// Type-safe version with Codable
func get<T: Decodable>(_ path: String, parameters: [String: Any] = [:]) async throws -> T {
guard let url = URL(string: path) else {
throw NetworkError.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}
// Model
struct UsersResponse: Codable {
let users: [User]
}
struct User: Codable {
let id: Int
let name: String
let email: String
}
// Usage (async/await)
Task {
do {
let response: UsersResponse = try await NetworkManager.shared.get(
"https://api.example.com/users",
parameters: ["page": 1]
)
print("Fetched \(response.users.count) users")
} catch {
print("Error: \(error.localizedDescription)")
}
}
Why this translation:
- Singleton:
static letsimpler than dispatch_once Result<T, E>more type-safe than separate parameters- Modern Swift: async/await preferred over callbacks
- Codable provides type-safe JSON parsing
- Error handling:
throwsinstead of error parameters - Generic version with Codable removes need for casting
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language exampleslang-objc-dev- Objective-C development patternslang-swift-dev- Swift development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- GCD, async/await, actors across languagespatterns-serialization-dev- JSON, Codable, NSCoding across languagespatterns-metaprogramming-dev- Runtime, reflection, property wrappers
Related conversion skills:
convert-swift-objc- Reverse conversion (Swift → Objective-C)