Feet in Both Worlds: from Objective-C to Swift

Objective-C Swift interoperability, and some useful hacks to make it work

andy_matuschak

Note: This video has some audio issues- we've done our best to fix them but they may still be noticable. We apologize for this and hope that you will enjoy Andy's excellent talk! / The Swift Summit Team

Transcript:

Andy: This morning we're gonna talk about Objective-C and Swift interop. We're gonna talk about it really from two perspectives. We're gonna talk about it from a theoretical perspective, that of language design. What does Objective-C interop do to Swift? And then we're gonna talk about what it was like for us to take a big, not big, 20,000 to 30,000 line Objective-C app, and start adding 20,000 to 30,000 lines of Swift to it, and to add those lines in a very Swifty style. The first history lesson; I forget sometimes just how old some of these technologies are. 1983, I may or may not have been born at that time, many of you in the room may or may not have been either. The point is, the technologies that we're using even today have a really, really long legacy. That legacy is valuable because there's a great deal of thought that has gone into all of these things, all of those things which begin with 'NS', those started being developed in 1988, and even such concepts as Cocoa bindings came from 1994's Enterprise Objects framework, Core Data, of course, inspired by that as well.

01:24: So when we got Swift, 30 years later, there was a desire to throw away some of the old. Objective-C was showing its age, it was being outstripped by languages on all other platforms, but there wasn't necessarily a desire to throw away all of the thought. So really what we have here with this lengthy history is both a liability, it's really old, it's long in the tooth, but also an advantage because implementing undo is ridiculously easy on Apple's platforms, and many of these APIs are actually really well thought out, and we wanna keep using them. We have tens of millions of lines of code of Objective-C, both of frameworks and of apps, we want to keep those things around, we want to be able to add to them incrementally, while also taking some ideas from the last several decades of language design. So like I said, we're gonna talk about theory, we're gonna talk about practice. Those are really the two sides of it, and I hope that you'll indulge me a little bit here on this theory side. I know that like the idea of, "Oh, what would it be like if we designed Swift?" Or, "How might Swift be designed differently?" it’s kind of an indulgent topic, it's not relevant per se, but it's an interesting thought exercise, and I feel that, to some degree, it perhaps informs the way that I use the language.

So what is the weight of the Objective-C Swift interop? It's actually pretty easy to measure, you can measure it physically 'cause there's a book. There's the book on the Swift programming language, and then there's a book on the interop between between Swift and Objective-C, like that's the weight of this thing, and it's 200 pages. So we start with this keyword, @objc, and we're looking at the Swift language, and we're saying. "You know what? What wouldn't have been there, if we hadn't had Objective-C, what would this keyword?" And the syntax part isn't important, what's important is the semantics that this pretends, because you can't put this keyword on just anything. You can put this keyword on things which are allowed to be bridged to Objective-C. And there's a list of rules, that list is several pages long, and developers really must memorize that list. You really have to keep it in your head all the time when you're working with Swift, otherwise you'll start to go ahead and think, "Oh, geez, I wrote this thing, and it's not gonna bridge. And now I'm stuck and I have to unwrite the thing or do one of the hacks, that I'm gonna show you later. There are hacks.

04:02: So you really, you have to keep that whole list in your head, and worse, you have to keep that list in your head irrespective of whether you're actually writing any Objective-C yourself because you're using a whole lot of frameworks, which are written in Objective-C, and which rely on the Objective-C run time, and which rely on being able to perform interesting queries on your objects at runtime, being able to see whether an object performs selector, and so on and so forth.

So this one little thing is sort of the tip of this larger iceberg that is the mapping layer. That mapping layer has huge semantic weight, hundreds of pages as we mentioned. Here's just a few of the things that you must keep in mind when moving back and forth between these lands. The boxing and un-boxing rules for NSNumber and Int are very interesting because, of course, they're unidirectional. An Int can become an NSNumber when it goes across the boundary, but an NSNumber does not become an Int, whereas some of these other things are actually bi-directional, id and AnyObject are interchangeable in both directions. NSArray and Array, of course, have even more interesting and complex semantics because NSArray is heterogeneous, it can contain elements of multiple different types, whereas Array is homogeneous, and when you cast an NSArray to an Array, at runtime it has to perform a check to make sure that that's legal and to figure out the degenerate type of the composite array. OptionSetType is even more complicated. I don't know if any of you have dived into that. All this is to say, the specifics don't particularly matter, merely that there's a lot of weight here, and this weight would not have existed if we did not care about this interop.

NSManaged is an interesting piece of weight, because it exists for the purpose of one framework. It exists for Core Data. I think this one might represent the liability advantage dynamic, perhaps more interestingly than any of the others. There's this framework called Core Data that a lot of people like to use, and it's valuable, that's an advantage. It provides a jump start to developers and their needs and persistence, and in having an object-relational mapping, if you believe in that idea. But it's also a liability. The design of Core Data relies on runtime semantics which are not only not idiomatic in Swift, they don't even really exist in Swift. The type checker fails if you try to use the kinds of semantics that Core Data requires. And so we had to introduce, they had introduce, a extra keyword into Swift just to deal with this NSManaged. And this is not so bad, cause if you don't use Core Data this just goes away. But I bring it up because it begins to introduce this secondary issue that we're going to examine, which is that the interop semantics are not only about the language itself, but rather about the libraries and frameworks which relate and which are part of the ecosystem. Core Data is bridged to Swift, but it does not belong in Swift. Core Data's semantics are alien there, they don't make any sense. You never would've written that framework if you'd started from Swift. So it's in this very strange liminal zone.


07:34:NSCopying is a related example, something that you would never have had if you'd started with Swift. Many of you probably don't even know the semantics, that's okay, it exists and it adds to the weight and complexity of the language. But I guess the costs that are more interesting to me, are the ones which don't go away as soon as you cease to use Objective-C. Because as we look at how this game's gonna play out, Objective-C's dead, right? It's a zombie, and so it's gonna shamble along for a while because there's a lot of residual, I don't know, ATP in it's system or whatever pushing it along. There's a lot of inertia and money that demands that it be pushed along. But it's awful and it's been doomed for a while, and now it's death sentence is finally cast. And so it's gonna die... Good. What we're gonna be left with is Swift, and so now we have to ask once this parasite/helpful thing is gone, what are we left with? What's the shape of the hole that it leaves? Okay, so here's something really frivolous. Just as a warmup for this exercise, let's just look at the design of external versus internal parameter naming in Swift. All of the arguments except the first one, the external and internal parameter, are the same except in the first one. The keyword specifies only the internal argument and the external argument is to be part of the stem of the function name. This is not something that ever would have been designed this way if it weren't for Objective-C.

This exists because of square bracket syntax, and that's why we're here. It's syntactic and it doesn't really matter, but I like to look at it because here's a great counter point. This of course.. init does not behave the same way, and there are other elements which don't behave the same way. So you have to keep these rules in mind. And there isn't a straight forward path to undoing these rules once Objective-C finally stops shambling and falls over. Dynamic, similarly, is an interesting one to me. This gets a little bit further below the surface. Dynamic is a keyword that's not often used, I think, in Swift, and probably for good reason. But it's one which opts a particular method, property, whatever, into runtime visibility so that something can reflect, something can introspect and it can say, "Hey what're are all the properties and methods on this thing. Maybe I'm gonna change them, maybe I just want to create some UI out of that or something." But the interesting thing about this property, is that it's contract... Is that it exposes those things via the Ob-C run time.

10:39: Now exposing these things via the Ob-C runtime has a lot of additional semantics bundled up in it. The Ob-C runtime, the way that it's designed, requires a great deal of locking around the accesses or modifications to any property modified in this way. And furthermore, it was written a very very very long time ago and modern reflection systems, even those employed in modern Java, have come a long way since then. There could have, perhaps, been an attempt made to define a modern runtime, but because of the weight of this thing that we still have, that wasn't done. So, now Dynamic just means the 30 year old run time. It's evolved a little bit, but the fundamental design remains the same. Now I'm gonna say something more controversial, I have no internal inside knowledge about this whatsoever, but I think the fact that it's possible to write a cycle that leaks in Swift is ridiculous and abhorrent, and it never ever, ever should have been designed that way. And I don't know what would have happened, we can't talk about would haves very concretely, but we can say that if we ask the question, "Why isn't there a cycle detector in Swift?"

Very rapidly, the answer becomes, "Well, what would the semantics be across the bridge?" We tried that once, it didn't work out. It was really, really, really costly and it was awful for everybody involved and it got thrown out, as you may remember. So now we're stuck with this thing, where people are invisibly making the performance of the system worse all the time, and there are tools to help you fight it, but most people don't use them. And that's just the world that we live in. That's cool. But I guess what worries me more is the general opportunity cost of all the paths that weren't taken, that couldn't be taken because of this bridge. The fact that there's sub-typing and sub-classing in this language at all, is an interesting thing to consider given the presence of protocol extensions. If we'd started a new environment with only protocols and protocol extensions, would we still have object-oriented style inheritance? It really, really complicates the semantics of the language and adds a lot of runtime performance cost that protocol extensions do not. The complexity distinctions between methods and functions and closures are also interesting to ponder, would those still exist if we hadn't started from this place of it has to interoperate with Objective-C?

13:32: The idea that value semantics are tied to structs and reference semantics are tied to objects is another one that's interesting to ponder. We could decouple those things, we could say define objects which had value semantics, or define things with value semantics, which have object-like behaviours, say inheritance or one of these other things. But we don't and we can't. Other languages do have those things, but Swift doesn't. Even the distinctions around mutating are very interesting. Why is it the case that this mutating keyword which has very special and interesting semantics for structs, cannot be defined for classes in Swift? Well, it's because of the semantics of classes in Objective-C, I would argue. So, it's interesting to consider all of the paths that were not taken and are not visible to you, to consider as well, which are visible only when you look at other languages in the ecosystem, which I encourage all of you to do. And now, having finished this brief exploration of the language itself, we can return our attention to the library and to adoption within it. 

The decision to make Swift interoperate deftly with Objective-C is absolutely the correct one. I can't imagine a universe in which Swift would be successful in which that decision were not made. And therefore adoption has been strong, adoption has been swift, we might say.


Adoption in libraries is another thing though, right? Because you have to look at the Standard Library as it grows and grows, and you look at foundation, and you say like, "Well, okay, where's the line supposed to be between those? And why do they have such different semantics and idioms?" And then, when you look at AppKit and UIKit and we think, "These APIs do not feel good in Swift." It's not because of the syntactic sugar of calling across idioms, it's because those APIs depend on many, many, many shared owners and inheritance, both of which are discouraged by new semantics in Swift, and do not take advantage of newer, nicer semantics in Swift. So we have to ask, at what point are we gonna move these libraries over to these new semantics? Who's gonna drive that? What's the timeline look like? What does that adoption strategy look like? I don't have any answers there, but I think it's time that we, the community, started talking about it, because I'm certainly not satisfied sitting around and developing against Objective-C style libraries for the next N years. 

16:18: Okay, that's theory. Let's talk about practice. This is a quote from the Swift book, this is a lie from the Swift book. It's a nice lie. I mean, it's kind of true. It's kind of true if you write bad Swift. But then, it's basically just syntax sugar versus Objective-C, which is nice, but which is maybe not worth the complexity. This clock is lying to me, I think this clock was assuming I was doing a 15-minute talk, but this talk is going to be 25 minutes, so I'm gonna ignore it. That's the plan, y'all. Okay, so we had this plan, because the Swift book told us this thing, and we believed it. And we embarked about 13 months ago on a large project, we being Khan Academy, to write a large product in Swift, large existing code based, running a large new product. And we had a whole bunch of very principled and ideological people who wanted to write it in a very Swift style. So we said, "All right, it's gonna be great." 

It was not great, it was emphatically not great. So along the way, we were constantly forced to rewrite large swathes of our app in Swift as we went, or we were forced to decide not to use Swifty idioms and semantics, which is not a decision that we really enjoyed having to make. I think it's fine. I'm not saying, "Oh, you shouldn't use Swift in existing apps." But what I am saying is, "You should add a lot of padding to your estimates.” 'Cause it slowed everything down a lot, for months. Many, many, many months. And so there's people out there who are just saying "Well, screw it. I'm just gonna rewrite the whole thing in Swift." I don't know, that might have been a better strategy. Transliterating Objective-C to Swift is pretty easy. 

18:22: So let's talk about strategies, and by strategies I mean hacks. 'Cause really these are the things which screwed us. Because if you're writing Swifty-Swift, you're gonna use a bunch of these things. You're probably also gonna use protocols and protocol extensions which I'm not even gonna talk about here 'cause I don't have any hacks to make that situation good in Objective-C. That situation is just terrible in Objective-C. So instead I'm gonna talk about things where you can do something about it. Enums, Structs, and generics. Let's start with enums.

So let's say that you have an Icon, and it can be either a Color or an Image, maybe it's a color when you're downloading content from the internet, you don't have the image yet. And you want to be able to use this view from Objective-C because you're trying to adopt inside out say, or because there's a new UI spec you're implementing and you think, "Well, I'm gonna implement the new thing in Swift and I'm gonna leave the old stuff the same, right? That's what they say you're supposed to be able to do." Well you can't construct an Icon from Objective-C because it is a enum with associated values and therefore you can't really use this view from Objective-C. So here's a dumb hack: You can just make multiple heads for the initializer. And for all of these dumb hacks, I've done my best to try to put them in an extension so that they can be ripped out as soon as the Objective-C support is no longer needed anymore. We can still let the Swift style initializer be the designated initializer, the primary initializer, and these two can just call through it as a bridge.

20:01: Another thing that might be relevant is maybe you've implemented your view in Swift now, because we're kinda implementing the new stuff in Swift as we go. But you still have this view controller that's in Objective-C and now the view controller is in a bit of a bind because the view needs to be able to consume this type that the view controller cannot express. So you can do a similar thing here, right? And what I'm pointing out is there's a bit of virality here. You want to be able to adopt Swift at any point in your application but that virality kinda works its way up the chain. This is the way that we dealt with it for enums and it worked okay. None of this is gonna be great. Another interesting problem is maybe your view controller needs to be able to hold on to the icon for a minute because the view is gonna be lazily instantiated later or something. Like you need storage.

So Box is your friend with many of these hacks. Many of you are probably familiar with this type. But if you're not, it's a useful type for getting around the bridge because box is an object and it is just represented as id on the Objective-C side. So here we can now have storage for this icon thing that Objective-C cannot represent and we can write an extension in the Swift which allows it to be accessed more sensibly. And that means that now this class is gonna be part Swift and part Objective-C, which does portend pretty much all of the problems that you're imagining. If you need to be able to actually expose setters for this thing externally, as you're moving up the stack, this is kinda the pattern that we settled on where you're gonna have two variants. You're gonna have iconColor, iconImage, setting one unsets the other. And these things thankfully don't have to deal with the boxing goo, because they can just use the boxing goo we previously defined.

22:13: Alright. If you have a simple enum like this one, this enum doesn't have any associated values, then you can split the Swift-only bits, like the nested type here and the computed property, into an extension. So you can define an Objective-C style suit enumeration and then in Swift you can extend that enumeration with extra Swift-only features. Moving on to struct now, our strategy will look pretty similar. So say again that we're trying to build that new UI in Swift. Well again we can't construct this new UI from Objective-C. But we can, if we make a shim constructor that looks like this. So this is pretty much what we did.

And similarly, moving up the stack, you look at your view controller which has the new Swift view and it needs to consume the new type, well okay you're gonna have to make multiple arguments now. So instead of multiple methods, that's what you get for some types for enums, you have a single method with multiple arguments, that's what you get for product types for structs. And similarly, if your view controller up the stack needs to hold on to an instance, you can use that boxing strategy like we used before. That works identically. And if you want to be able to expose a setter for some element within that user you can now write a setter which consumes the boxing property. Now if the struct is simple, here the struct doesn't take advantage of any Swift only features except for the method defined on it. Similarly, we can define the struct in Objective-C and then we can define an extension on it which supplies the method. In this particular instance, if we needed to use the method from Objective-C or from C, we could accommodate that as well by defining this function just in C. And now we're making something that's, is kind of like Core Graphics. That's pretty much where we've gotten.

24:28: Generics. Happy news here. Generics were recently improved significantly in Swift 2 with respect to the Objective-C bridge, because you can now make a generic type, which conforms to an Objective-C protocol. This was a classic case that people wanted to be able to do in Swift because Swift is data-driven. Swift likes values and UITableViewDataSource is a really annoying protocol, so people want to be able to make an abstraction to deal with that. So here is the attempt at an abstraction where you just have some class that hangs on to some values that supplies them when the thing asks. And this was not possible until very recently because you weren't allowed to make a Swift generic class which conformed to an Objective-C protocol. That's because the storage semantics of that thing in Objective-C inside of the table view, inside of that i-var slot that was gonna hold on to this guy, that was not clear.

Anyway, that's been fixed up and you can now actually define this in Swift 2, which is a really lovely thing. But you can't instantiate this from Objective-C, and you also can't call any methods on it from Objective-C. You get to treat it as a UITableViewDataSource, you can call UITableViewDataSource methods, but you can't call any methods defined on the type which conforms to UITableViewDataSource. So, there's a trick that I like. Here's a really simple interface for a future. I think someone's talking about futures later. That'll be cool. A future is a thing which represents a value that is gonna be emitted asynchronously, because we're gonna fetch a thing from the network and then we're gonna emit the thing later. So, you can subscribe to a future and if you're the network request, you can fulfill a future by giving it a value and then all the subscribers, they get called.

26:23: The subscriber, its action takes an optional value because the future might have been cancelled. This is a super lame quick slide attempt at that API, right? So the API semantic is that if the future is cancelled, then the subscribers' closures are invoked with nil as the argument. That's the semantic. So, the problem that we have now is the structure of our application is such that in order to implement new Swift bits incrementally, we are going to need to hang on to a bunch of these futures corresponding to network requests which are related to a particular view controller, and when that view controller is destroyed or is gonna go off the screen or whatever, we want to cancel all of the futures related to that view controller. We wanna be able to call the cancel method on this generic class from Objective-C.

So, the pattern that I like is one that you've actually seen in the Swift library before and the sort of name for this pattern, is type erasure. So, what we're doing here is we're erasing the value type inside of the future, we're making an AnyFuture, it's a future that emits something, we don't know what, and that means that we can't subscribe via this interface because it might emit a bool, it might emit an int, we don't even know. So, we can't define a reasonable subscribe method but we can define a reasonable cancel method. The interesting thing here to observe is that this class doesn't have to be generic because none of its properties are generic. It's initialized as generic, so you can't construct this class from Objective-C but you can call the cancel method on it because the class stores a closure which has captured a generic value. That's my trick with generics.

So, in conclusion, Objective-C's days are numbered, and it is interesting to imagine what we will be left with when that number finally ticks down to zero. I think that this is a responsibility that it's not just for Apple to have but is for all of us to have as we participate in this community and in this landscape. Thank you very much.


Q&A (28:35):


Q1: For those of us with large apps, how would you go about the decision making process of rewrite versus you know, carefully…

Andy: ...Yeah. What I've been advising people more recently has been probably bite the bullet and do it incrementally depending on your situation. So, if you have a whole bunch of a really intricate code and it's gonna be dangerous to rewrite it or really time-consuming or you don't have the resources, then don't rewrite, do it incrementally but plan on being slow for a while because you're gonna have to write these shims, you're gonna have to rewrite stuff along the way, but you're not gonna rewrite everything along the way. Just plan to be slow. If you have the resources to rewrite and, I don't know, your app isn't 300,000 lines or something, it might not be so bad to rewrite. Transcribing is easy.

Q2: Hi Andy, thanks, great talk. The example you gave of the enum, of the Icon, with the Color and the View, or the Color and the Image, did you guys try at all using protocols to abstract away the Swift type? So like something like icon-displayable which could be extended, like you could extend an image to be icon-displayable or a color?

Andy: That's a cool idea. I like that. So, just playing that forward a little bit for people in the audience, I guess the idea would be that on Icon we would implement a protocol called Icon Displayable. And Icon Displayable would define say, I don't know, a display on view method or display on image view method and some extension would... For the color instance would say set the background color of that view to the color and for the image instance would set the image, on that view of the image. I think because I have this orthodoxy in my head around what values should and shouldn't be doing, that idea did not occur to me because I in general don't want values to be...

Audience: To not have behaviours?

Andy: Yeah, I want them to be inert. But, given that we're already making hacks, this is another hack to consider.

Q3: And to be greedy, I do have another question. The interop, or I guess having the hybrid classes that are both implemented in Objective-C and Swift, did you run into any issues where you have an extension of a view controller, for example, where you would like to have a property that is of a Swift type but Swift extensions can't add storage properties?

Andy: Yeah, yeah, for sure, for sure, its awful. You make global lookup tables. I'm serious. In general, you rewrite the type in Swift, that's what you do.