In this Altconf talk, Nikita Lutsenko dives deep into how you can build apps and frameworks using both Objective-C and Swift at the same time, the current limitations and benefits of this approach, and what is coming in Swift 3.0. Working and leading Apple platforms SDKs at Parse and Open-Source at Facebook, including the first Facebook contribution to Swift - BoltsSwift, as well as the SDK and the realtime client for Parse, KVOController, and the pop animation library, this talk takes examples from all of the work mentioned above.
Introduction (0:00)
Interoperability is actually a very tricky word, but what it means is bridges. If you imagine that one bridge is Objective-C, and the other one is Swift, your job is to plug both those bridges together, so they fit and work nicely.
Today I’m going to talk to you about three things.
- How to bridge Objective-C to Swift: what are the tools, what Swift 3 adds to it
- How to bridge Swift back to Objective-C. (You don’t really wanna do it, but if you want to)
- How to make both mix and match together and what are the pitfalls of using them together in one code base.
So first, let’s start with Objective-C to Swift.
Objective-C to Swift (0:58)
Turns out that bridging Objective-C to Swift just works. That means that 99% of APIs written in Objective-C are already imported into Swift and available for you. There is also a focus from Apple, as well as everyone in the community, on making Objective-C APIs have Swift-y names, better type safety, and overall, just go and rewrite everything with grunt-rename
into Swift 3. That 1% that is not available in Objective-C to Swift is NSInvocation
and NSMethodSignature
.
NSInvocation()
NSMethodSignature()
NSOperation(invocation: )
NSProxy.methodSignatureForSelector()
...
NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
If you don’t know what this is, great, please don’t. If you do, it’s not available. If you want to use that, you still can with Swift (we will get to that) but just know that all of the APIs that accept invocations or return method signatures are not available, and Apple doesn’t want you to use that. So let’s see what is available! Let’s see how can we make existing Objective-C code better in Swift 2 and 3.
ObjC to Swift 2.* (1:49)
There are a lot of different ways to make the code better, but I’m going to focus first on:
- Nullability annotations
- Error handling: what is different, hidden, what we did not see coming when we saw new error handling introduced in Swift 2.0 last year.
- Cast macros, which are:
NS_SWIFT_NAME
NS_SWIFT_UNAVAILABLE
NS_REFINED_FOR_SWIFT
NS_SWIFT_NOTHROW
Easy, right? Let’s take a look at this example:
// ObjC
- (BOOL)performRequest:(NSURLRequest *)request;
- (BOOL)performRequest:(NSURLRequest *)request error:(NSError **)error;
We have a network controller or some custom controller in your application in Objective-C, and we have two methods: one that we added a long time ago, and one that we added recently. It’s all synchronous, so the second method accepts a pointer to a pointer to NSError
and returns a boolean. It turns out, this is what it looks like when we import it into Swift 2:
// Swift 2.*
public func performRequest(request: NSURLRequest!) -> Bool
public func performRequest(request: NSURLRequest!, error: ()) throws
That is actually not that great. If you want to use the second method, you have to pass void
for the error because it accepts void
. Why? Because the built-in Swift transpiler from Objective-C, Swift importer, cannot reason about these types if you have both selectors. So let’s make it better and more Swift-y.
Nullability Annotations (3:54)
The first thing that we’re going to do to this API in Objective-C is add nullable
annotations:
- (BOOL)performRequest:(nullable NSURLRequest *)request;
- (BOOL)performRequest:(nullable NSURLRequest *)request error:(NSError **)error;
public func performRequest(request: NSURLRequest?) -> Bool
public func performRequest(request: NSURLRequest?, error: ()) throws
Nullability annotation is something new in Objective-C. It was added in Xcode 6.3.2 last April, and it allows you to annotate all of your APIs and tell everyone to expect something that can be null, or that will never be null. You can add them by using nullable as an annotation to an NSURLRequest
, or you can mark an entire region as NS_ASSUME_NONNULL
. For example:
NS_ASSUME_NONNULL_BEGIN
- (BOOL)performRequest:(NSURLRequest *)request;
- (BOOL)performRequest:(NSURLRequest *)request error:(NSError **)error;
NS_ASSUME_NONNULL_END
public func performRequest(request: NSURLRequest) -> Bool
public func performRequest(request: NSURLRequest, error: ()) throws
As you can see in both of these examples, the first one, everything becomes optional instead of implicitly unwrapped optional. In the second example, it’s not optional anymore. Awesome, right? We can reason about things.
The next one to make any Objective-C API better in Swift is NS_SWIFT_NAME
.
NS_SWIFT_NAME (4:50)
- (BOOL)performRequest:(nullable NSURLRequest *)request NS_SWIFT_NAME(perform(request:));
- (BOOL)performRequest:(nullable NSURLRequest *)request error:(NSError **)error NS_SWIFT_NAME(perform(request:));
public func perform(request request: NSURLRequest?) -> Bool
public func perform(request request: NSURLRequest?) throws
NS_SWIFT_NAME
gives you an ability to provide a full Swift name for an Objective-C class, NS_ENUM
, Constant, or function. You use it by passing the Swift name for the selector that you want to import. In our example, we just made the Swift imported API way better. Even though I actually use the same name for both, they’re not gonna be imported the same way into Swift. See, the first one returns a BOOL
, the second one uses throws
since they are two different functions.
Let’s mark something unavailable!
NS_SWIFT_UNAVAILABLE (5:53)
What if we want to get rid of the first method? It will still be there, just not exported into Swift, so you can focus on writing awesome code. We can do that by adding NS_SWIFT_UNAVAILABLE
on top:
- (BOOL)performRequest:(nullable NSURLRequest *)request NS_SWIFT_UNAVAILABLE("");
- (BOOL)performRequest:(nullable NSURLRequest *)request error:(NSError **)error NS_SWIFT_NAME(perform(request:));
public func perform(request request: NSURLRequest?) throws
It turns out that this also has an impact on the existing code. If we don’t add NS_SWIFT_NAME
to the second argument but just mark the first as unavailable, that removes the error pointer, or error void parameter. Because now Swift can actually reason about your code and what you’re trying to do. What you’re trying to do is just have a method that throws or accepts a pointer to a pointer to NSError and returns a Boolean or any other value. But please don’t, please always mark things with NS_SWIFT_NAME
because performRequest
is not Swift-y, but perform
and request
as a name for the argument is way better.
public func perform(request request: NSURLRequest?) throws
NS_REFINED_FOR_SWIFT (7:06)
Let’s take a look at something ugly:
// ObjC
- (void)getFirstName:(NSString **)firstName
lastName:(NSString **)lastName;
// Swift
func getFirstName(firstName: AutoreleasingUnsafeMutablePointer<NSString?>,
lastName: AutoreleasingUnsafeMutablePointer<NSString?>)
The Objective-C function will extract a first and last name and will put that into pointers that you passed into it. So it’s a pointer to a pointer to an NSString
. That is not very widely used, but when it’s imported into Swift, we get this crazy thing which is called AutoreleasingUnsafeMutablePointer
for NSString
optional. Whew. And this is terrible API to import into Swift, please don’t. Even if we add NS_SWIFT_NAME
it’s not better:
// ObjC
- (void)getFirstName:(NSString **)firstName
lastName:(NSString **)lastName
NS_SWIFT_NAME(get(firstName:lastName));
// Swift
func getFirstName(firstName: AutoreleasingUnsafeMutablePointer<NSString?>,
lastName: AutoreleasingUnsafeMutablePointer<NSString?>)
We still have AutoreleasingUnsafeMutablePointer
for NSString
optional. What we can do is use something that is called NS_REFINED_FOR_SWIFT
:
// ObjC
- (void)getFirstName:(NSString **)firstName
lastName:(NSString **)lastName NS_REFINED_FOR_SWIFT;
// Swift
extension Person {
var names: (firstName: String?, lastName: String?) {
var firstName: NSString? = nil
var lastName: NSString? = nil
__getFirstName(&firstName, lastName: &lastName)
return (firstName: firstName as String?, lastName: lastName as String?)
}
}
What NS_REFINED_FOR_SWIFT
will do for you is add a double underscore to the function name that is imported into Swift. But since double underscores are there, and usually we don’t start methods in Swift with that, what it’s going to do is remove that from the typehead inside Xcode, as well. So instead in Swift, we can use things like tuples and return a tuple, or FirstName, LastName, for tuples of our names, and inside of it, actually use NSString
, strings, and unwrap or rewrap everything for us.
That is one of the craziest parts of importing Objective-C with Swift. See how I need to use NSString
optional, and I cannot simply use string optional? It turns out, with Unsafe Mutable Pointers, I cannot do that, and the compiler will warn me about it. So I need to extract them as NSString
s and then wrap them or cast them to strings themselves.
But this is nothing new, so let’s talk about something new.
What is new in Swift 3? (9:27)
The first thing that I want to mention is the grand rename, as Apple mentions it in their keynotes. That is the rewriting of the foundation, all of the core graphics, to give it way better Swift names. There are still some frameworks that are not really Swift-y. We can expect that more and more APIs are going to be rewritten which are going to be using value types instead of reference types.
Also, Swift 3.0 brings in Objective-C lightweight generics. There are some limitations, of course. There is also NS_NOESCAPE
, and CF_NOESCAPE
. If you’ve you’ve seen @noescape
in any Swift functions, and it’s great! It’s available, and I’m going to show you how to use it.
And the last one is a pretty amazing one, which is NSEXTENSIBLESTRING_ENUM
. Terrible name, but such a fantastic feature. So first, let’s talk about Objective-C generics.
Objective-C Generics (10:47)
@interface MyNetworkController<Request: NSURLRequest *> : NSObject
- (void)performRequest:(nullable Request)request;
@end
What we have here is once again MyNetworkController
, and I have a generic type or generic parameter, on the class itself, which is Objective-C like generics. You cannot have a generic parameter on the function itself, or on the enum, but you can have it on classes. Right now, in Swift 2.0, the code above is imported as:
public class MyNetworkController : NSObject {
public func performRequest(request: NSURLRequest?)
}
In Swift 3, what’s going to happen is this:
public class MyNetworkController<Request : NSURLRequest> : NSObject {
public func perform(_ request: Request?)
}
So now, we actually do have a generic parameter on the MyNetworkController
, yay! That broke a lot of our code! Now, we can perform(_ request: Request?)
. Also notice, I did not use NS_SWIFT_NAME
, but Swift 3 properly imported the name. It actually rewrote the name and was able to match request in both cases, and see that performRequest
is just perform
and request
the first argument.
Great, what are the limitations of this approach?
- You cannot use any extensions.
- You cannot use a generic outside of Objective-C lightweight generics.
- You cannot extend a class and use a different generic type.
- You cannot extend based on just generic type that conforms to a class.
It’s pretty limited, but a good thing that we finally have it in some form.
NS_NOESCAPE/CF_NOESCAPE (12:23)
- (void)executeActions:(void (NS_NOESCAPE ^)(void))actions;
func executeActions(_ actions: @noescape () -> Void)
The next one is NS_NOESCAPE
. So what does an NS_NOESCAPE
mean? (It’s actually imported as @noescape
.) It means that any argument that is passed inside this closure, which is void
for void
is not going to escape the runtime of the function itself. It makes a lot of sense for the compiler to optimize out these things because it knows that: you won’t pass this out of the block. You won’t pass this out of the closure. And the lifetime of any object that is passed into the closure is not going to outlive the runtime of the function itself.
- (void)executeActions:(void (NS_NOESCAPE ^)(void))actions NS_SWIFT_NAME(execute(actions:));
func execute(actions: (@noescape () -> Void))
Also, since executeActions
is not Swift-y, please use NS_SWIFT_NAME
, it’s very helpful.
NSEXTENSIBLESTRING_ENUM (13:24)
The last one, about Objective-C Swift bridging and importing, is going to be NSEXTENSIBLESTRING_ENUM
.
So, in Objective-C we usually see this construct:
typedef NSString *NSRunLoopMode;
extern NSRunLoopMode const NSDefaultRunLoopMode;
Right now in Swift 2 and 3, they’re going to be simply imported as typealias
and let
constant:
// Swift 2
public typealias NSRunLoopMode = NSString
public let NSDefaultRunLoopMode: String
But in Swift 3, by adding NSEXTENSIBLESTRING_ENUM
, magic happens:
typedef NSString *NSRunLoopMode NS_EXTENSIBLE_STRING_ENUM;
extern NSRunLoopMode const NSDefaultRunLoopMode;
// Swift 3
struct RunLoopMode : RawRepresentable {
public static let defaultRunLoopMode: RunLoopMode
}
So now, RunLoopMode
by itself is RawRepresentable
, while it also represents a string. It has an associated type, and every single constant with the same type in Objective-C will now become public let defaultRunLoopMode
, for example, in this case.
So why is it useful?
It’s useful for RunLoopMode
s where NSString
s or NSURLConnection
s, or anything else, say NSTimers
, that depend on RunLoopMode
s, which can enforce type safety, and use only RunLoopMode
, when they’re importing something, versus any other NSString
.
We all have hundreds of notification constants or user default keys. And when you import them into Swift, you can ensure type safety and in-place use only, versus use any other string.
To sum up Objective-C to Swift import, my recommendation to everyone, is:
- Use nullability annotations. They add a lot of safety to your Objective-C code. Even without importing into Swift, your API is going to become much better.
- Add Objective-C generics to classes where they can be genericised.
- Do the four r’s. Rename, refine, rinse, repeat.
NS_SWIFT_NAME
,NS_REFINED_FOR_SWIFT
,NS_SWIFT_UNAVAILABLE
, and repeat.
Swift to Objective-C (15:55)
I love building bridges. So, let’s build another one. Let’s build the Swift to Objective-C importing.
Turns out there is not much available. We all love tuples, we all love generics, type aliases, Swift enum, Swift structs, global variables, free-standing functions. Basically, nothing is available.
Nothing that is Swift-specific is available in Objective-C, which is pretty sad. Meaning that you usually don’t want to go the path of importing Swift to Objective-C, because the only thing you will be able to use is subclasses of NSObject. You can still subclass UIViewControllers (these can still be exported), but you cannot add more things. At the moment, if you add a generic type to a view controller it won’t be exported into Objective-C.
The rules are, everything that is:
- not
private
, - not the new attribute called
fileprivate
(private
is scope-only, andfileprivate
is inside file) - not explicitly marked as
@nonobjc
- any @objc protocol
Can be imported into Objective-C. Turns out that if you add @objc
, it will actually just tell you what can you do and what you cannot.
So, subclasses of NSObject
will be explicitly exported to Objective-C. Sometimes you don’t need to add one; I actually recommend doing it now and then, because you can also validate using this approach in which you can export something.
What else can we export?
We can export concrete extensions and classes. Say, my class conforms to UIApplicationDelegate
. I can export that class because that’s a concrete extension.
What I cannot export is an extension on a view controller where an element conforms to a custom string convertible because Objective-C has no idea how to actually do that. So @objc protocols are there. There’s also only one kind of enum, which is int enums. You cannot export anything else, like RawRepresentable
’s, string
’s. The stuff that we love. No, only in enums.
@objc protocols (18:26)
Let’s talk a little bit more about Objective-C protocols.
What Objective-C protocols are in Swift is very special. There is so much that was added specifically for it. It’s also weaker and not Swift-y, even though you can use optional
. So, optional
is not supported on Swift protocols, unless they are exported into Objective-C, because that time, they become Objective-C protocols.
There is also no ability to talk to extensions of these protocols. Either they are concrete extensions, with no way you can talk to a protocol and build things like protocol extensions, which we all love and use in Swift. So they’re very limited altogether.
If you actually are writing Swift code, please don’t use these, they make everyone’s life bad as well as they could not be used, say, in a Linux environment, where you don’t have Objective-C runtime.
Value Types (15:55)
Let’s talk about something more advanced. Something with an underscore before it. Value types via _ObjectiveCBridgable
. If you are curious about what powers foundation, it’s this. We’ve seen this before, and it’s an implementation detail. Meaning that you can rely on it right now, as long as you embed Swift runtime into your application. That may last for another year because we don’t have API stability just yet. It gives you an ability to bridge native Objective-C types with native Swift types.
So what does the protocol look like? How does it work?
It looks like this, which is ugly:
public protocol _ObjectiveCBridgeable {
associatedtype _ObjectiveCType : AnyObject
static func _isBridgedToObjectiveC() -> Bool
func _bridgeToObjectiveC() -> _ObjectiveCType
static func _forceBridgeFromObjectiveC(_ source: _ObjectiveCType, result: inout Self?)
static func _conditionallyBridgeFromObjectiveC(
_ source: _ObjectiveCType,
result: inout Self?
) -> Bool
static func _unconditionallyBridgeFromObjectiveC(_ source: _ObjectiveCType?) -> Self
}
But if you look at all the methods here, it’s literally a single method to bridge back to Objective-C, as well as to, force conditionally and unconditionally, a bridge from Objective-C. It also always has associatedtype
requirement, which if we want to import index path, we can say our associated type, Objective-C type, is NSIndexPath
. It turns out that you can bridge any type from Objective-C to any struct, enum, or class in Swift, one-to-one. And when they talk to Objective-C, they will be represented as Objective-C types. When they talk to Swift, they will be passed in and automatically bridged for you into Swift.
So, let’s talk more about bridging Swift and Objective-C together, and the pitfalls of this approach.
Pitfalls of Bridging Swift and Objective-C (21:22)
So, the first thing that is going to be surprising to you is closure to block performance. Turns out that using closure to blocks (mapping closures to blocks, or passing closures as blocks into Objective-C) slows down the performance of using this approach by about fifteen percent.
Great, right?
The reason is that closures are a super-special case, like a custom type, built in Swift. Where in Objective-C, all of them are Objective-C objects. So to bridge Swift closure into Objective-C, Swift needs to create an Objective-C object for you, which is not usually the default case. So it slows down the performance.
Also, there are many pitfalls about type compatibility that are not obvious. Objective-C runtime behaves very strangely, and very different, depending on whether you call in from Swift, where we have static dispatch usually, or you call in from Objective-C, where we have dynamic dispatch only.
Type Compatibility (23:24)
So first, let’s talk about type compatibility.
NSArray<NSString *> *names = @[ @"John", @"Jane" ];
NSArray<id <NSCopying>> *array = names;
That is a typical case, where you have an NSArray
of NSString
names. On top of this, we want to upcast it, meaning that we want to “unspecify” it into something that is an NSArray
of objects that conform to the NSCopying
protocol. We know that NSString
conforms to it so that we can do that. It turns out that in Objective-C, generics are runtime only. They are not compiled. They don’t have a representation in your code, in the binary, in assembly. And they’re there only for the compiler, as well as for the Swift importer.
let names: [String] = ["John", "Jane"]
let array: [CustomDebugStringConvertible] = names
If we look at the same example in Swift, where we take something, like an array of String
’s, and we upcast it to an array of a protocol that is implemented by NSString
. This code will compile, but when you run it, what we’re gonna see is this:
fatal error: can't unsafeBitCast between type types of different sizes
So it turns out that this fails only at runtime, but never fails at compile time. The reason is that CustomDebugStringConvertible
is a separate type in Swift because it’s a protocol. Since protocols in Swift have different memory layouts than structs, they don’t have just struct memory layout, they also have witness tables for it. So you need the extra witness table storage for it to be able to represent that in an array.
Since we’re simply casting an array of strings, which are structs, which don’t have custom witness value table, on top of it, it fails at runtime, because it cannot fully represent it. So in our particular case, you’re creating an array of strings, the compiler doesn’t create extra space for witness table, then you want to use that as an array of protocols. It doesn’t really work.
What works is this:
let names: [String] = ["John", "Jane"]
let array: [CustomDebugStringConvertible] = names.map({
$0 as CustomDebugStringConvertible
})
You fix it by explicitly casting every single member of that array to the explicit type, and the protocol value witness table is added, just at that line, when you do the cast. It knows that it needs it so that the compiler will add it there for you.
Let’s look at something else. Let’s look at Objective-C runtime in Swift. The case where it doesn’t really work:
@objc class Animal: NSObject {
let name = "Animal"
override init() {
super.init()
}
}
@interface Plant: NSObject
@end
@implementation Plant
- (NSString *)name {
return @"Plant";
}
@end
So we have two classes. An Animal
and a Plant
. They both have a selector that is called name
. All of this is exported into Swift, and inside Objective-C. So we can use it from both places.
Next thing, let’s swizzle the methods out:
// ObjC
SEL selector = @selector(name);
Method originalMethod = class_getInstanceMethod([Animal class], selector);
Method swizzledMethod = class_getInstanceMethod([Plant class], selector);
method_exchangeImplementations(originalMethod, swizzledMethod);
// ObjC
NSLog(@"I am %@!", name);
// Swift
print("I am \(name)!")
That is a default use case, exchange implementations, which will change the actual function that powers it, the actual pointer to the piece of code, to run separately. Do you expect this to print the same thing, or not? This should print “I am plant,” right? So, it does it for Objective-C, but it doesn’t do it for Swift. So, Swift still wants to be a dog.
// ObjC
SEL selector = @selector(name);
Method originalMethod = class_getInstanceMethod([Animal class], selector);
Method swizzledMethod = class_getInstanceMethod([Plant class], selector);
method_exchangeImplementations(originalMethod, swizzledMethod);
// ObjC
NSLog(@"I am %@!", name); // I am Plant!
// Swift
print("I am \(name)!") // I am Animal!
Great.
@objc class Animal: NSObject {
let name = "Animal"
override init() {
super.init()
}
}
@interface Plant: NSObject
@end
@implementation Plant
- (NSString *)name {
return @"Plant";
}
@end
Because we did not declare let
as dynamic, Swift will always use static dispatch. It will never go the dynamic dispatch route. Because Objective-C has no idea how to use static dispatch or dynamic dispatch, it will always take in a dynamic dispatch case. So, to fix it, what you need to do is, for anything that you want to explicitly swizzle, add dynamic
before it:
@objc class Animal: NSObject {
dynamic let name = "Animal"
override init() {
super.init()
}
}
@interface Plant: NSObject
@end
@implementation Plant
- (NSString *)name {
return @"Plant";
}
@end
You can do it for functions, properties, variables, and classes. Now, when we run it, what we’re gonna see is “I am plant” in both cases.
// ObjC
NSLog(@"I am %@!", name); // I am Plant!
// Swift
print("I am \(name)!") // I am Plant!
Okay, so, let’s sum up. Swift and Objective-C interoperability is hard, and building bridges is not as easy as you might think it is. But we all love this. You don’t actually have to do this. You can skip it and go build everything in Swift.
Which is what you should do: migrate to Swift as early as possible.
But in cases where you use OpenGL graphics, please don’t. In cases where you interact with C++, you actually cannot do that at all. There is no compatibility with C++. When you want to use system calls, please don’t. Use C, in this case, because it’s still better. Or you want super uber high performance. Also, please don’t bridge C++ strings, they are still way more powerful than whatever Swift 56 times performance is.
Q&A (29:07)
Q: My biggest problem in bridging between Swift and Objective-C was using Swift protocol in Objective-C. I have a delegate protocol declared in Swift, and I want to use it in the header for Objective-C. But I can’t use the umbrella header from Swift to use it because I would then import the umbrella header, and the header file does not support it. Do you have any suggestions?
NL: You had a problem with using a delegate protocol from Swift inside Objective-C, and when you imported the umbrella header, which is the “–your-product-name” Swift header, in this case, it was not exported into it. It’s not possible to import the umbrella header in a header file in Objective-C. So it is possible, but if you then import this header file in another implementation file, where this umbrella header is imported, then it gives an error. So what I would recommend is building that protocol, if you need to use it from Objective-C, inside Objective-C, and then importing back into Swift. That would work as the best resort. It turns out that the delegate from my use case, and from my experience, delegate as a protocol, or as a construct, in object-oriented programming doesn’t really work amazingly in Swift. Specifically, with Swift protocols, because all of the methods become required at once, and unless it’s an Objective-C protocol, nothing can be optional. So, using a delegate in Objective-C code is a good practice. Create one in Objective-C, use it from Swift, migrate everything to Swift when you can get rid of it.
Q: When you upcasted from in the string to the protocol, and it crashed during runtime, if instead of having the string inside the array you would cast just the let
variable, to another let
variable, then would the compiler complain?
NL: If you would cast a let
variable to another let
variable, it should not complain, because you can pass, at the same time, any variable to another variable. It could complain if it doesn’t do automatic bridging for you, and doesn’t cast that automatically for you; it will complain in that case, or it will do automatic casting for you.
Q: So you don’t even have to do it manually?
NL: Yeah. Unless it’s an array, because it does not cast everything for you in the same way, then you don’t need to do this. Object collections in Swift are very, very, very special. You can also fix that problem by casting it to NSArray
first, and then casting back, which is crazy. Please don’t ever do that, you’ll lose so much performance.
Q: Do you have any strategies for dealing with mixed code bases containing Objective-C and Swift, that need to be compatible with iOS 9 and iOS 10?
NL: If you want to migrate over into Swift 3.0, I would recommend spending a week or two just doing that. The approach that I’m going to encourage for anything open-source that we do at Facebook is we’re going to set a marked date, and we’re gonna move to Swift 3.0 with GM from that point.
If you want to use Objective-C and Swift, together, I would migrate module by module. Say, a sign-up flow, has four view controllers, custom views, and everything, move all of that. The most benefit that you will gain from Swift, from moving from Objective-C to Swift, is in the model part of your MVC application or view-model part. You won’t get much benefit if you still need to do CocoaTouch, and deal with UIKit, and any other framework there, because that is still bridged to Objective-C.
Q: I’ve never seen NS_NOESCAPE
before. So if I understood this properly, if you markup block with this, the block would not leave farther than this method? Or the method is being passed?
NL: You are declaring that nothing that you pass in, into this closure, or the closure itself, will extend the lifetime of the call to this method/function.
Q: So, these would be, for example, as a completion block that gets passed another time. And so, a good way of making the API work correct, probably?
NL: This is a terrible case for passing a completion block, please don’t use it. Say, UIView performs without animation, actually, the new iOS 10 SDK added that there because there is no way the block or anything passed into the block will extend the lifetime of perform without animation call. Or execute actions that the one that I had, if you had a block of actions that you want to execute, that will work.
Q: You mentioned delegate protocols not being a good design pattern in Swift. Could you say more about that?
NL: Sure. So, the part about delegate protocols that we all love is that they give us the power to implement just one or two methods, and be interested in just one or two things. Turns out that natively, in Swift protocols, if we are not talking about @objc
protocols in Swift, there is no way we can mark methods there as optional. Because the protocol should be fully implemented, meaning that it’s basically anything that uses Objective-C runtime in Swift has the performance decrease, because it needs to do the bridging in between the other one. And using a delegate protocol with all the methods, as required is probably not the best case. The case that I’ve used and the one that I replace bridging actual Objective-C into Swift is creating custom closures. Custom closure provides as weak, for every single method that you’re interested in. You can also have name tuples inside the closures as arguments. So that basically replaces a direct pointer to a class/instance that implements the delegate method.
Q: I wanted to ask an important question here. Once you go with Swift, why would you ever go back to Objective-C?
NL: I will not comment on that, but that is super accurate. Also, if you have huge Objective-C codebase, such as Facebook that has two hundred thousand more classes. We have the biggest app out there ever. We bundle in our own open SSL for good reason.
Why would you go back to Objective-C? OpenGL, C++, super uber high performance, system calls. That’s the only four reasons that came to my mind when I was writing this. If you find another good use case for going back to Objective-C, tell me more!
Q: Do dynamic libraries increase the delay when launching an app? Let’s say you have thirty CocoaPods, all written in Swift, and if you instantiate those, when the app launches it actually takes longer to do it, launching a dynamic library as opposed to a static library.
NL: Okay, it’s half-way there. So, if you force link everything, every single dynamic library, which is what CocoaPods is doing by default, you’re absolutely right. You’re going to take a performance hit. Because at the time, you are paging into your binary, you’re loading your binary; you’re going to open every single framework that you have there. It turns out that you don’t need every single framework out there, and if you weak link, the linker is so smart that it will open things when you first call them. Of course, if you do Objective-C runtime, that will not be the case. Also, it turns out that using static libraries also gives you a performance hit because all of the pointers in static libraries. Since this is not single-object linking, will be wrong, and when you just page in the binary what the kernel needs to do is go into your binary and fix all the pointers to every single code or block that you linked in. Which you might think is not a huge thing, right, but Facebook loads almost a second slower at a cold start just because of that.
Q: Can you tell us any best practices of migrating to Swift, or how do you handle this at Facebook?
NL: We don’t migrate to Swift. That’s the best way to handle it, right? We don’t migrate because we have Objective-C tooling. Plus, we need C++. We cannot do a lot of things without C++. Plus we have React Native, which helps a lot of product engineers.
But for the best cases of migrating from Objective-C into Swift, I would say take user features, user-facing things, and migrate all at once. Or take an object-oriented part of your application, say a full model layer, and migrate that into Swift. What you can do is also have a single API and stick to one API and that API needs to be implemented in Swift when you migrate over. Migrating portions of your application will actually help you a lot since it’s not going to be a huge mess all over the place. A good example would be migrating dock, which Apple talked about. It’s in what’s new, either in Swift or in Swift with foundation. Where they migrated just portions and we’re building new features only in Swift.
Receive news and updates from Realm straight to your inbox