In this tutorial, we’ll explore how Swift and Objective-C can work together in a single project. We’ll use a demo app to illustrate key concepts of interoperability between these two languages.
Project Setup
First, let’s set up our project structure. We’ll have the following files:
- SwiftObjCInteropDemoApp.swift (Swift)
- SwiftPerson.swift (Swift)
- ContentView.swift (Swift)
- Person.h (Objective-C)
- Person.m (Objective-C)
- ObjCUser.h (Objective-C)
- ObjCUser.m (Objective-C)
Step 1: Creating the Swift App Entry Point
Let’s start with our app’s entry point in Swift:
// SwiftObjCInteropDemoApp.swift import SwiftUI @main struct SwiftObjCInteropDemoApp: App { var body: some Scene { WindowGroup { ContentView() } } }
This is a standard SwiftUI app structure.
Step 2: Defining a Swift Class for Objective-C Use
Next, let’s create a Swift class that we’ll use from Objective-C:
// SwiftPerson.swift import Foundation @objc class SwiftPerson: NSObject { @objc let name: String @objc private(set) var age: Int @objc init(name: String, age: Int) { self.name = name self.age = age super.init() } @objc func introduce() { print("Hi, I'm \(name), a Swift person aged \(age).") } }
Key points:
- The
@objc
attribute makes this class and its members visible to Objective-C. - We inherit from
NSObject
, which is required for most interop scenarios. - Properties and methods we want to expose to Objective-C are marked with
@objc
.
Step 3: Creating an Objective-C Class
Now, let’s create an Objective-C class that we’ll use from Swift:
// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy, readonly) NSString *name; @property (nonatomic, assign) NSInteger age; - (instancetype)initWithName:(NSString *)name age:(NSInteger)age; - (void)sayHello; @end // Person.m #import "Person.h" @implementation Person - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { if (self = [super init]) { _name = [name copy]; _age = age; } return self; } - (void)sayHello { NSLog(@"Hello, I'm %@, an Objective-C person aged %ld.", self.name, (long)self.age); } @end
Step 4: Creating the Bridging Header
To use Objective-C code in Swift, we need a bridging header:
// SwiftObjCInteropDemo-Bridging-Header.h #import "Person.h" #import "ObjCUser.h"
Make sure to set this file as your project’s Objective-C Bridging Header in the build settings.
Step 5: Implementing the Main View
Now, let’s create our main view in Swift:
// ContentView.swift import SwiftUI struct ContentView: View { @State private var outputText = "" var body: some View { VStack { Text("Swift-Objective-C Interoperability Demo") .font(.headline) .padding() Button("Run Demo") { runInteropDemo() } .padding() ScrollView { Text(outputText) .padding() } } } private func runInteropDemo() { var output = "" // Using Objective-C class in Swift let person = Person(name: "John Doe", age: 30) person?.sayHello() output += "Objective-C Person created and said hello.\n" // Extending Objective-C class in Swift person?.celebrateBirthday() output += "Person celebrated birthday.\n" // Using Swift class let swiftPerson = SwiftPerson(name: "Jane Smith", age: 25) swiftPerson.introduce() output += "Swift Person introduced themselves.\n" // Demonstrate Swift optional handling if let personName = person?.name { output += "OBJC Person's name: \(personName)\n" } // Using Swift closure with Objective-C let swiftClosure: @convention(block) (String) -> Void = { message in output += "Message from ObjC: \(message)\n" } // Using Objective-C class that uses Swift let objcUser = ObjCUser() objcUser.useSwiftPerson() output += "ObjCUser used SwiftPerson.\n" // Pass Swift closure to Objective-C objcUser.useSwiftClosure { message in output += "Received in Swift: \(message ?? "")\n" } outputText = output } } extension Person { func celebrateBirthday() { age += 1 print("Happy Birthday! \(name ?? "") is now \(age) years old.") } }
Key interoperability points:
- We use the Objective-C
Person
class directly in Swift. - We extend the Objective-C
Person
class with a Swift method. - We create and use a
SwiftPerson
instance. - We demonstrate optional handling with Objective-C properties.
- We create a Swift closure to pass to Objective-C.
Step 6: Creating an Objective-C Class That Uses Swift
Finally, let’s create an Objective-C class that uses our Swift code:
// ObjCUser.h #import <Foundation/Foundation.h> @interface ObjCUser : NSObject - (void)useSwiftPerson; - (void)useSwiftClosureWithBlock:(void (^)(NSString * _Nullable))block; @end // ObjCUser.m #import "ObjCUser.h" #import "SwiftObjCInteropDemo-Swift.h" @implementation ObjCUser - (void)useSwiftPerson { SwiftPerson *swiftPerson = [[SwiftPerson alloc] initWithName:@"Alice" age:28]; [swiftPerson introduce]; } - (void)useSwiftClosureWithBlock:(void (^)(NSString * _Nullable))block { if (block) { block(@"Hello from Objective-C!"); } } @end
Key points:
- We import the
-Swift.h
header to use Swift classes in Objective-C. - We create and use a
SwiftPerson
instance in Objective-C. - We define a method that takes a block (closure) as a parameter.
Conclusion
This tutorial demonstrated key aspects of Swift and Objective-C interoperability:
- Using Objective-C classes in Swift
- Extending Objective-C classes with Swift methods
- Creating Swift classes that can be used in Objective-C
- Handling Objective-C optionals in Swift
- Passing Swift closures to Objective-C
- Using Swift classes in Objective-C
By understanding these concepts, you can effectively work with projects that use both Swift and Objective-C, whether you’re maintaining legacy code or gradually migrating to Swift.