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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/convert-objc-swift/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| | Swift String is value type |
| | Platform-dependent signed integer |
| | Direct mapping |
| / | Generic array with type safety |
| | Use for mutability |
| / | Generic dictionary |
| / | Type-erased reference |
| / | Existential or opaque type |
| / | Explicit optionality |
| | Return type is class of receiver |
| | Closure type |
| | Weak reference |
| (for strings) | Immutable by default |
| | 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
with specific types or genericsid - Test equivalence - Same inputs → same outputs
Type System Mapping
Primitive Types
| Objective-C | Swift | Notes |
|---|---|---|
| | Direct mapping (true/false vs YES/NO) |
| | Platform-dependent signed (32-bit or 64-bit) |
| | Platform-dependent unsigned |
| | Explicit 32-bit signed |
| | Explicit 32-bit unsigned |
| (64-bit) / (32-bit) | Platform-dependent |
| | Explicit 64-bit signed |
| | 32-bit floating point |
| | 64-bit floating point (default) |
| | Platform-dependent float (bridged) |
| / | C char type |
| | Unicode character |
| / | Unit type |
| | Return type of current class |
Foundation Types
| Objective-C | Swift | Notes |
|---|---|---|
| | Bridged; Swift String is value type |
| (with ) | Swift strings mutable with var |
| / specific type | Bridge to Int, Double, Bool when possible |
| | Untyped array |
| | Generic array with type safety |
| (with ) | Mutable via var declaration |
| | Untyped dictionary |
| | Generic dictionary |
| (with ) | Mutable via var |
| | Untyped set |
| | Generic set |
| | Bridged value type |
| | Bridged value type |
| | Bridged value type |
| / | Conforming to Error protocol |
Nullability and Optionals
| Objective-C | Swift | Notes |
|---|---|---|
| | Implicitly optional in Objective-C |
| | Explicitly nullable |
| | Non-optional |
| | Optional |
| Non-optional | Default in Swift |
| | Same keyword, different semantics |
Blocks and Closures
| Objective-C | Swift | Notes |
|---|---|---|
| | No parameters, no return |
| | Single parameter |
| | Multiple parameters |
| | Type alias |
(implicit) | (explicit) | Escaping closures marked in Swift |
Collection Types
| Objective-C | Swift | Notes |
|---|---|---|
| | Generic array |
| | Mutable array |
| | Generic dictionary |
| | Mutable dictionary |
| | Generic set |
| | Mutable set |
| Custom (no direct equivalent) | Use array with uniqueness logic |
Composite Types
| Objective-C | Swift | Notes |
|---|---|---|
| | Class declaration |
| | Protocol adoption |
(extension) | | Class extension |
| | Category → Extension |
| | Protocol definition |
| | Property |
| | Immutable property |
| | Weak reference |
| / | Copy semantic (Swift strings already copy) |
| | Enumeration |
| | Integer-backed enum |
| | Option set |
| | Type alias |
| | 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
declaration@property - Memory semantics:
is default,strong
explicit,weak
unnecessary for value typescopy - Initialization required for all non-optional stored properties
for immutable,let
for mutablevar
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:
is thread-safe and simpler than dispatch_oncestatic let - Completion handlers: prefer
over separate parametersResult<T, E>
must be explicit for closures that outlive function scope@escaping
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
for safe unwrappingguard let- 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
for class-only protocols (enables weak references)AnyObject - No
/@required
- use protocol extensions for default implementations@optional - No runtime checks needed with type-safe protocols
for existential types (heterogeneous collections)any Protocol
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:
→ SwiftNS_ENUM
with raw valueenum
→NS_OPTIONS
struct for bitwise optionsOptionSet- 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 (
) instead of error pointersthrows - Custom error types conform to
protocol (usually enums)Error
for error handlingdo-catch
for optional conversiontry?
for explicit error valuesResult<T, E>- 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
required for stored closures@escaping- Prefer
over separate data/error parametersResult<T, E> - Swift async/await preferred over callbacks
/[weak self]
pattern cleaner than weak-strong danceguard let self
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:
attribute exposes Swift declarations to Objective-C@objc- Swift class must inherit from NSObject (or @objc class)
- Xcode auto-generates
headerProjectName-Swift.h - Not all Swift features available in Objective-C (generics, protocols with associated types, etc.)
@objc Attributes
| Attribute | Purpose | Example |
|---|---|---|
| Expose to Objective-C runtime | |
| Custom Objective-C name | |
| Expose all members of class | |
| Hide from Objective-C | |
| Interface Builder outlet | |
| Interface Builder action | |
Type Bridging
Foundation types bridge automatically between Objective-C and Swift:
| Objective-C | Swift | Bridging |
|---|---|---|
| | Automatic, toll-free |
| | Automatic |
| | Automatic |
| | Automatic |
| , , | Automatic (context-dependent) |
| | Automatic |
| | Automatic |
| | Automatic |
| | 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 |
| Protocols with associated types | Use type-erased wrappers |
| Tuples | Use structs or separate parameters |
| Enums with associated values | Use separate classes or |
| Value types (struct) | Use classes ( subclass) |
Optionals (except via ) | 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 |
|---|---|---|
| (default) | Strong reference |
| | Weak reference |
| / | Swift Strings/Arrays already have value semantics |
| (value types) | Primitive types |
| | Weak in closures |
| Default | Strong in closures |
| | 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 |
| Swift compiler | Compiles Swift code |
| Swift REPL | Interactive Swift | command in terminal |
| 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
is simpler for pure datastruct - No need for manual init - memberwise initializer is free
- Value semantics (copy) is default for structs
for immutable propertieslet
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
for existential typeany DataSourceDelegate- 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:
simpler than dispatch_oncestatic let
more type-safe than separate parametersResult<T, E>- Modern Swift: async/await preferred over callbacks
- Codable provides type-safe JSON parsing
- Error handling:
instead of error parametersthrows - Generic version with Codable removes need for casting
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Objective-C development patternslang-objc-dev
- Swift development patternslang-swift-dev
Cross-cutting pattern skills:
- GCD, async/await, actors across languagespatterns-concurrency-dev
- JSON, Codable, NSCoding across languagespatterns-serialization-dev
- Runtime, reflection, property wrapperspatterns-metaprogramming-dev
Related conversion skills:
- Reverse conversion (Swift → Objective-C)convert-swift-objc