| name | convert-objc-swift |
| description | Convert Objective-C code to idiomatic Swift. Use when migrating Objective-C projects to Swift, translating Objective-C patterns to modern Swift, modernizing legacy codebases, or establishing gradual migration strategies with interop. Extends meta-convert-dev with Objective-C-to-Swift specific patterns including bridging and @objc attributes. |
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)