Claude-skill-registry convert-objc-swift

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-objc-swift" ~/.claude/skills/majiayu000-claude-skill-registry-convert-objc-swift && rm -rf "$T"
manifest: skills/data/convert-objc-swift/SKILL.md
source content

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-CSwiftNotes
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

  1. Analyze source thoroughly - Understand memory management, protocols, categories
  2. Map types first - Create comprehensive type equivalence table
  3. Preserve semantics - Match behavior, not just syntax
  4. Adopt Swift idioms - Don't write "Objective-C code in Swift syntax"
  5. Handle edge cases - nil messaging, NSNull, dynamic typing
  6. Leverage type safety - Replace
    id
    with specific types or generics
  7. Test equivalence - Same inputs → same outputs

Type System Mapping

Primitive Types

Objective-CSwiftNotes
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-CSwiftNotes
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-CSwiftNotes
id
Any?
Implicitly optional in Objective-C
nullable id
Any?
Explicitly nullable
nonnull id
Any
Non-optional
_Nullable
?
Optional
_Nonnull
Non-optionalDefault in Swift
nil
nil
Same keyword, different semantics

Blocks and Closures

Objective-CSwiftNotes
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-CSwiftNotes
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-CSwiftNotes
@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
    @property
    declaration
  • Memory semantics:
    strong
    is default,
    weak
    explicit,
    copy
    unnecessary for value types
  • Initialization required for all non-optional stored properties
  • let
    for immutable,
    var
    for 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 let
    is thread-safe and simpler than dispatch_once
  • Completion handlers: prefer
    Result<T, E>
    over separate parameters
  • @escaping
    must 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 let
    for 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
    AnyObject
    for class-only protocols (enables weak references)
  • No
    @required
    /
    @optional
    - use protocol extensions for default implementations
  • No runtime checks needed with type-safe protocols
  • any Protocol
    for 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
    → Swift
    enum
    with raw value
  • NS_OPTIONS
    OptionSet
    struct 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
    Error
    protocol (usually enums)
  • do-catch
    for error handling
  • try?
    for optional conversion
  • Result<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
  • @escaping
    required for stored closures
  • Prefer
    Result<T, E>
    over separate data/error parameters
  • Swift async/await preferred over callbacks
  • [weak self]
    /
    guard let self
    pattern 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:

  1. Incremental migration: Convert one class at a time while maintaining a working app
  2. Bidirectional calling: Swift can call Objective-C, Objective-C can call Swift
  3. Shared types: Foundation types bridge automatically
  4. 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:

  • @objc
    attribute exposes Swift declarations to Objective-C
  • Swift class must inherit from NSObject (or @objc class)
  • Xcode auto-generates
    ProjectName-Swift.h
    header
  • Not all Swift features available in Objective-C (generics, protocols with associated types, etc.)

@objc Attributes

AttributePurposeExample
@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-CSwiftBridging
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 FeatureWorkaround
GenericsUse specific types or
id
Protocols with associated typesUse type-erased wrappers
TuplesUse structs or separate parameters
Enums with associated valuesUse separate classes or
NS_ENUM
Value types (struct)Use classes (
NSObject
subclass)
Optionals (except via
_Nullable
)
Use nullable annotations
Protocol extensionsExpose 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:

  1. Bridging header (Objective-C → Swift):

    • Build Settings → "Objective-C Bridging Header"
    • Path:
      ProjectName/ProjectName-Bridging-Header.h
  2. Generated interface (Swift → Objective-C):

    • Automatic:
      ProjectName-Swift.h
    • Import in Objective-C files
  3. 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-CSwiftNotes
@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
DefaultStrong 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

ToolPurposeNotes
Xcode migration assistantAutomated Swift conversionUse as starting point, manual refinement needed
swiftc
Swift compilerCompiles Swift code
Swift REPLInteractive Swift
swift
command in terminal
swift-demangle
Demangle Swift symbolsDebug symbol names
SwiftLintSwift style linterEnforce Swift conventions
Objective-C → Swift converter (Xcode)Edit → Convert → To Modern Objective-C Syntax firstImproves 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
    struct
    is simpler for pure data
  • No need for manual init - memberwise initializer is free
  • Value semantics (copy) is default for structs
  • let
    for 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 DataSourceDelegate
    for 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 let
    simpler 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:
    throws
    instead 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 examples
  • lang-objc-dev
    - Objective-C development patterns
  • lang-swift-dev
    - Swift development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - GCD, async/await, actors across languages
  • patterns-serialization-dev
    - JSON, Codable, NSCoding across languages
  • patterns-metaprogramming-dev
    - Runtime, reflection, property wrappers

Related conversion skills:

  • convert-swift-objc
    - Reverse conversion (Swift → Objective-C)