스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Refine Objective-C frameworks for Swift
Fine-tune your Objective-C headers to work beautifully in Swift. We'll show you how to take an unwieldy Objective-C framework and transform it into an API that feels right at home. Learn about the suite of annotations you can use to provide richer type information, more idiomatic names, and better errors to Swift. And discover Objective-C conventions you might not have known about that are key to a well-behaved Swift API. To get the most out of this session, you should be familiar with Swift and Objective-C. For more on working with Swift and Objective-C, check out our Developer Documentation and take a look at “Behind the Scenes of the Xcode Build Process” from WWDC18.
리소스
-
다운로드
Hello and welcome to WWDC.
Hi. I'm Brent from the Swift Compiler team. I'm gonna talk to you today about how to make your Objective-C frameworks work really nicely in Swift. We first introduced Swift six years ago this month. Since then, something amazing has happened. Most of you have turned into Swift developers. But Apple's platforms spent a very long time as Objective-C-only platforms, and a lot of you couldn't adopt Swift right away. So, many of you still have a lot of Objective-C code today that's increasingly being used by Swift clients.
We understand that because Apple is in the same boat. We probably have more Objective-C frameworks than anyone in the world. So, we designed Swift to not only import our Objective-C frameworks, but also to automatically translate them into more idiomatic Swift APIs. We designed in ways to customize the translation by adding macros and keywords to your header files.
And we built in the ability to extend Objective-C frameworks with custom Swift code to wrap existing APIs or add new ones that can't be expressed in Objective-C.
Best of all, we designed all of this to work not just for us, but for you. That's my topic for today. I'm going to teach you the techniques that Apple's Objective-C frameworks use to improve how they're imported into Swift.
And to do that, I'll be using a little framework I call SpaceKit.
Let's take a look at it.
SpaceKit is an Objective-C framework which has a few model APIs for describing NASA's early, crude space program. So it can describe, you know, astronauts, missions, rockets and so on. If I look at the headers, it's clear that this is an Objective-C framework full of #import directives, @interface blocks, methods with selectors. But if I import it into Swift, the compiler will automatically translate it into a Swift API, and I can see what that API will look like right in Xcode by looking at the header's generated interface. All I need to do is open the Related Items menu in the top left of the editor...
go down to the Generated Interface submenu...
and then ask for the Swift 5 interface, since 5.3 is the Swift version in Xcode 12. And what I end up with is basically what a Swift header file would look like if Swift had header files. You can see that it has this class, there's some initializers, some properties, some methods. But I don't see the bodies of these declarations, just the interfaces. This is a really valuable tool to understand what Swift will see when it imports this framework.
I'm going to be showing you a lot of SpaceKit's generated interfaces, these sort of pseudo headers, throughout this talk.
If you poke around a little in them, you'll see that Swift is already doing some great stuff for you. For instance, these parameters were all NSString and NSDate objects in Objective-C, but the compiler has bridged them into the structs you would use in Swift.
It's imported init methods as initializers, rewritten method names into a style closer to Swift and turned methods that follow the Objective-C error handling convention into throwing members. But there's also some room to improve this framework. The API is peppered with these implicitly unwrapped optionals.
The "Any" in "AnyHashable" types in these collections are really vague.
This throwing method will sometimes throw when it shouldn't. And this method's name may not quite be perfect.
Looking elsewhere, these two initializers seem oddly redundant.
And this NSError information will be difficult to use with try-catch.
And if you move away from objects and take a look at a more C-style part of SpaceKit, you'll see more work that needs to be done.
This type used NS_ENUM, so it comes in as a very nice Swift enum.
But these collections of string constants could use the same treatment.
And one of these constants has straight-up vanished.
These free-floating utility functions aren't ideal for Swift, and this UInt result will be a pain to work with. I'm going to walk you through how to improve all of that. I'll show you how to specify the types in your APIs more precisely to help your Swift clients use your framework correctly.
I'll talk about two very important Objective-C conventions that Swift assumes your APIs will follow.
I'll teach you how to correct situations where Swift might not import things that Objective-C can access.
And I'll help you put the finishing touches on your framework and make it feel like a good Swift citizen. So, let's get started...
with those implicitly unwrapped optionals that appeared virtually everywhere in the API. They leave really important aspects of your framework's behavior undeclared. So, where did they come from, and how do we get rid of them? Objective-C pointer types, including id-types and blocks, can have a valid value or they can have zero value, which we call "null" or "nil." This is a lot like Swift's optional type, which can either have value or be nil, except that in Objective-C, every pointer type is effectively optional and every nonpointer type is effectively nonoptional. But, of course, a lot of the time, a property or method won't actually handle a nil input or won't ever return a nil result. So, when Swift imports an Objective-C pointer type, by default, it marks it as an implicitly unwrapped optional to tell you that this value could be nil, but Swift isn't sure if it ever really is.
Fortunately, Objective-C provides two nullability annotations, nonnull and nullable, which let you say whether nil is a sensible value for a particular property, method parameter or method result.
Objective-C doesn't enforce these annotations. They just document your intent. But Swift picks up this information and uses it to decide whether to make a type optional or not.
You apply these annotations by editing your Objective-C header files.
In a property, the annotation goes in a list of the property's attributes.
In a method parameter or result type, it goes right before the type's name.
So, let's say you're starting with this name property. You decide that instances of this class may not always have a name, so you add the nullable annotation and the type in the generated interface changes to optional. Great. But then you hit "build" and you start seeing new warnings appear.
Don't panic. You didn't break anything. What happened is Xcode noticed that you started using nullability annotations in this header, so it starts telling you about all the places that you still need to fill them in.
So that gives you a nice workflow. Add the first annotation somewhere in the header, then work through it one warning at a time filling in either "nullable" or "nonnull" until all the warnings are gone.
Once you've done that, there's one more clean-up step you should do-- add the NS_ASSUME_NONNULL_BEGIN macro at the top of the file and the matching end macro at the bottom, then delete all the nonnulls between them. This just cleans up the file a little so that whenever you see one of these keywords, it always indicates you'll get an optional in Swift. When you're adding these, occasionally you'll come across a case where these annotations don't work, like this constant. If you try to put the nonnull annotation in front of it, you'll get a compiler error and the generated interface probably won't display at all. The annotations I showed you work with methods and properties, but anywhere else, like constants or global functions or blocks, you'll have to use the qualifier versions of these annotations instead.
These start with an underscore and a capital letter, and they work on any pointer type anywhere in Objective-C. You'll also need these for pointer-to-pointer types. In that case, you'll specify one keyword for each level of the pointer, so you can say that the inner pointer can be nil, but the outer one can't be, and Swift will nest the optional and unsafe mutable pointer types correctly. So, going back to our global, we apply the underscore qualifier, and it becomes nonoptional in Swift just like we wanted.
When you're doing this, you need to be careful that your annotations are correct. For instance, maybe you glance over this header and you decide, "Of course every mission is going to have a capsule. Let's make the capsule property nonnull." But it turns out later that there was a mission where they didn't launch a capsule, and you do model that by setting the capsule to nil. In other words, the nonnull annotation was wrong. So, what's gonna happen when Objective-C returns "nil" for a value Swift thinks can't be optional? Well, if it's an NSString or NSArray on the Objective-C side, there's a special case. You get an empty Swift string or array.
That could be a problem here because SpaceKit expects the string to equal one of its SKCapsule constants, and none of those equal empty string, so you'll end up with a value that you might not handle correctly.
For other types, it can get a lot weirder.
You can end up with an invalid object, something you usually can only get from unsafe operations.
If it's an Objective-C object, you might not even notice because Objective-C method calls ignore nils. But in some cases, you'll crash with a null pointer dereference or get other unexpected behavior, and the compiler doesn't promise anything about what happens, so switching to release mode or changing Xcode versions could change the symptoms of this kind of bug.
The important point is when you write in the header that something can't be nil, Swift doesn't force-unwrap it, so you won't see a crash at the place where it returned nil.
Swift doesn't second-guess your Objective-C headers. It believes what they tell it. The good news is the Objective-C compiler and the Clang Static Analyzer also look at nullability annotations and can point out many violations in your Objective-C code. So, if you say something can't be nil, but you have Objective-C code that makes it nil, these tools might tell you that something's amiss. Once you finish adding nullability annotations, it's a good idea to look for new warnings or static analyzer results in both the framework's implementation files and any Objective-C clients you have access to. Those results could indicate that you have mis-annotated, or they could be telling you about subtle bugs that you never knew were there. But suppose you see some warnings or analyzer results and can't quite decide if they can actually happen. Or you just had some really complicated old code and you can't figure out if there's an edge case where it will return nil. What should you do then? Well, earlier I mentioned the nonnull and nullable annotations. There's actually a third option called null unspecified which makes Swift import the value as an implicitly unwrapped optional. You should use this in all the places you'd use an implicitly unwrapped optional in Swift, like values that are only nil very early in an object's life cycle. But you can also use it when you think an API can't return nil but you're not sure, that way Swift clients can still use the result without unwrapping it. But if the framework does end up returning nil, you'll reliably get a crash at the use site instead of maybe getting some impossible-seeming misbehavior sometime later.
We've gone through our project and we've added all of the nullability annotations. Great. Next we'd like to deal with this array of "Any" and other collections that aren't bridging very well. It turns out Objective-C supports a generic syntax that's a lot like Swift's, so if you make it an NSArray of SKAstronauts in Objective-C, you'll get a Swift array of SKAstronauts in Swift. This also works with NSSet and NSDictionary, so it can really improve a lot of APIs. Next, let's go take a look at a different area of the project. Here we have a function called SKRocketStageCount, and, like you'd expect from the name, it returns a count. Since a count can't be negative, it returns it as an NSUInteger, which means that in Swift, it returns a UInt, and that means this function breaks a Swift convention. In both Objective-C and Swift, it's conventional to use unsigned types when an integer represents a collection of bits and you want to perform bitwise operations on those bits, or do some other computation where signed arithmetic might get in the way. Usually, when you're working at this level, you care about the exact number of bits in the value. NSUInteger's size varies by architecture, so people rarely use it in this way. Rather, the main reason people use NSUInteger in Objective-C is to indicate that a number's value is never negative. Objective-C enables this style with automatic conversions and carefully designed overflow behaviors, but these exact features can cause serious security bugs, so Swift doesn't include them. Instead, Swift requires you to explicitly convert unsigned types to signed if you wanted signed arithmetic, and stops execution if unsigned arithmetic would produce a negative result. This makes it more difficult to mix Int and UInt in Swift the way you might mix NSInteger and NSUInteger in Objective-C.
So, the conventional Swift style is to just not do that.
Instead, idiomatic Swift APIs use Int, even for values that are never negative.
For Apple's frameworks, we applied a blanket rule that all NSUIntegers turn into Ints when Swift imports them.
For your frameworks, it's your choice whether to update your headers to use NSInteger or not, but we recommend that you do.
It makes very little difference in Objective-C, but it has a huge impact in Swift.
If we look at the broader context, there's a bigger problem with this function, and it's that clients can misuse it very easily. The SKRocketStageCount function is supposed to be used with these SKRocket constants. That's what the matching names are supposed to indicate. But Swift doesn't know that because what Swift sees is this function takes a string. These constants are all strings, but there are a heck of a lot more strings out there too. And if you pass one of those to SKRocketStageCount, it probably won't do anything good. In a pure Swift framework, you could prevent this. You'd just turn these constants into an enum or a struct with a raw value of type String, and then change the function to take only that type. You could do that to SpaceKit by wrapping these APIs by hand, but there's a much easier way.
First, you introduce a new typedef to group the constants together and change all the places involving the constants to use that. By itself, this does practically nothing. A typedef gets imported as a type-alias in Swift, and in both languages, that's just an exact synonym for the original type. But that's just a preparatory step.
Next you add the NS_STRING_ENUM macro after the typedef. This dramatically reshapes the typedef in Swift. It now imports as a struct with the constants nested inside it, making something that looks and feels just like an enum with a raw string value.
And most importantly, it means that the StageCount function no longer takes arbitrary strings. It only takes instances of SKRocket. Mission accomplished.
You can use this feature to define your own custom string enums, but Apple's frameworks define a lot of them too.
I've listed a few common ones from Foundation here, but there are least 50 in the iOS SDK alone. It'd be a good idea to look for NSString parameters or constants in your APIs, which really ought to be one of these, and update them to match. Next, let's talk about some ways that you could get into trouble by not following Objective-C conventions correctly when Swift assumes that you will. When you look at the generated interface for SKAstronaut, you see something kind of funny. This class has two initializers. Both of them are passed a person's name, but in two slightly different forms.
One takes a PersonNameComponents, a Foundation type that has properties for things like given name and surname. The other take a string labeled "name." It stands to reason that one of these initializers probably calls the other. And yet if you subclass SKAstronaut, Swift will make you override both of them. That seems a little unnecessary.
But there's actually a second issue with this class's initializers, one that isn't visible in the generated interface.
If you look in SKAstronaut's code completion, you'll see a third initializer with no parameters. This init isn't in the generated interface and it's not in the original header either. It seems like it came from outer space. But in fact it came from here, from the super class.
SKAstronaut inherited it from NSObject, and even though your clients can call it, it may not actually work properly. These two problems have the same root cause.
In Objective-C, there's a convention for initializers which makes sure the clients know how to write a subclass that will always get initialized correctly.
The convention divides initializers into two categories-- designated and convenience.
You need to override all of the designated initializers to make it safe to inherit the convenience initializers. Now, if you're thinking, "Gosh, this sounds really familiar..." that's because it is. Swift classes use the same basic model for their initializers. There are some differences in the details. For example, you mark the designated inits in one language and the convenience inits in the other. But Swift classes have these same two categories of initializers, and they work in basically the same way.
But unfortunately, the biggest difference between the languages here is that in Objective-C, designated initializers are not a language rule. They're a convention that each class must choose to follow by marking at least one initializer as designated. And many Objective-C classes don't opt in. This means that clients don't know how to subclass your class.
That's not great for any class, but it's especially bad for frameworks because then clients have to read your source code or reverse engineer your behavior or just guess, and those are all good ways to end up with buggy subclasses.
That's bad enough, but it also means that you, the frameworks maintainer, don't get warnings if you forget to override something you want to.
If a client uses an initializer that you forgot to override, that means your class gets skipped over during initialization. So, ivars you thought would always refer to an object will be nil instead.
And even if you did override everything you needed to, mistakes are so common that clients can't really be sure of that. So, the first step in fixing this problem will be to opt in to this convention by marking your designated initializers in your headers.
If you're not sure which initializers should be designated, take a look at the implementation. Typically, designated initializers will call an init with "super," convenience initializers will call one with "self." So, by looking at their bodies, you can determine that an init With Name Components should be designated, and init With Names should be convenience.
With that in mind, you can go back to the header file and mark the designated initializers with NS_DESIGNATED_INITIALIZER while leaving the convenience initializers unchanged. In SpaceKit, you'll mark init With Name Components as designated, while leaving init With Name alone.
Once we're done, Swift will recognize that init Name is a convenience initializer and mark it with a convenience keyword.
At this point, you may start seeing warnings in your Objective-C implementation files about superclass designated initializers that you need to override.
These were latent bugs. If someone had used one of them, your object wouldn't have been initialized correctly.
If you want your class to support any of these initializers, just go ahead and implement them normally. But if you don't, implement an override that calls does Not Recognize Selector.
Then go back to the header file and declare it with the NS_UNAVAILABLE attribute. Then do the same thing for any superclass convenience initializers since they might call the one you disabled. Marking these initializers as unavailable is the equivalent to not inheriting them, which is what Swift would do automatically if you didn't override a designated initializer. With these changes, your Swift and your Objective-C clients will now know which initializers will work and which ones they need to override in their subclasses. That's a win all around.
Next, let's talk about the Objective-C error handling convention. Earlier, I said this method might throw when it shouldn't.
This doc comment, or rather the behavior it describes, is the reason. A lot of Objective-C developers misunderstand the error handling convention. They think that if a method wants to signal failure, it has to return "false" and set the error to a nonnil value.
A "false" return alone, they think, isn't a failure. It's just false or nil or whatever.
But that isn't actually the convention. The convention is, if a method returns a false value, that's a failure even if the error value is "nil." We really don't recommend leaving the error "nil" because then your caller has no idea what happened. But if you do, a "false" return is still a failure. When Swift generates a call to an Objective-C method it has imported with throws, it assumes that the method will follow this rule correctly, so it always throws if the method returns "false." Swift doesn't allow you to throw "nil," so if there is no error, Swift throws a nonpublic Foundation error type.
Because the type is not public, you can't write a catch statement for it.
But if you see this type and case in logs, the debugger or error messages, it means that some Objective-C code either returned "false" even though it didn't fail, or it failed but didn't tell you why.
So let's think about how that applies to this SpaceKit method. Its documentation says that it can return "false" in a situation where it has skipped work, but hasn't actually failed.
But Swift assumes that a false return value means it should throw an error, and since the method didn't set an error, it'll be one of those internal Foundation "nil errors" I mentioned. What can you do about that? Well, you have a few options.
The easiest is to simply remove the special case so that false always means failure and the method follows the convention properly. But that's probably not workable if clients actually need to detect this case. An alternative is to use NS_SWIFT_NOTHROW to tell Swift that you're not following the error convention. Swift will then import the method the normal way and you can write error handling code manually.
This still leaves the method kind of broken, but it might be a good solution if you're also planning to deprecate the method and write a better replacement.
Whether or not you kept the original around in deprecated form, that better replacement would need to change the method's signature so that it can follow the error handling convention while returning the extra information another way.
For instance, you could add a Boolean out parameter to say whether the file was actually saved. Then the return value can be used the way the error convention specifies. That's not perfect, but it's about the best you can do from just Objective-C.
But if you want to go a little further and write a bit of Swift code, you can get perfect Swift import from this method.
Let me show you how.
So, let's take a look at the header for SKMission. You can see I've been updating it as we go along and now it has the old deprecated method plus the new method with the extra parameter so it follows the convention.
Great. Now, let's write the better Swift version. To start, I need to add a Swift file to my project so I can put my Swift method in it.
I'll do that the same way I'd add an Objective-C file, but choosing the Swift template instead.
I don't need to import SpaceKit in this file because it's already part of the framework. But it doesn't automatically see every bit of Objective-C in SpaceKit.
What happens is that Swift automatically imports everything in SpaceKit's umbrella header. The umbrella header is the header with the same name as the framework, so for this framework it's SpaceKit.h.
Since the umbrella header imports all of the public headers in this framework, my Swift file will see everything declared in all of those headers. It's always a good practice to have an umbrella header in a framework, but it's especially important in a framework that'll be used from Swift. App and test targets don't have umbrella headers, so Xcode offers to add a special bridging header to those targets that serves this function. Quick aside here, there's one public header that I shouldn't import in the umbrella header.
And that's the generated header. The SpaceKit-Swift header which declares anything I mark with @objc in Swift. The problem is that this forms a circular dependency.
Swift can't make the generated header without first importing everything in my umbrella header.
So, if the umbrella header imports the generated header, then Swift will try to read this file that it hasn't generated yet. And that will just ruin your whole day.
So, don't import the generated header in your other headers, only in your implementation files.
Even though it's not in the umbrella header, Objective-C clients who have modules enabled will automatically import it anyway, so it should all just work out. Anyway, back to the task at hand.
I'll extend SKMission...
to add a new method called save to, which both throws and returns a Bool. I couldn't return the Bool in Objective-C because the Objective-C error convention takes over the return value. But that's an Objective-C convention.
In a Swift method, the return value is totally separate from whether or not the method threw an error, so there's no problem with this. Now, let's implement this. First, I'll need a Bool variable to receive the wasDirty value.
Next, I'll call the Objective-C method and finally, I'll return the wasDirty value.
Hit "Build" and... Oops, I have a type error.
What happened? Well, on some of our platforms, Swift's Bool and Objective-C's Bool actually have a slightly different memory representation. Normally, Swift inserts a little conversion to smooth this over. But here, you're trying to take a pointer to a Swift Bool and pass it off to Objective-C so that Objective-C can directly read and write it.
There's no way for Swift to insert a conversion there, so instead it uses a type called ObjCBool, which matches the Objective-C Bool representation. So, to make this work...
I need to change the variable's type to ObjCBool.
And then, in the return statement...
use its bool Value property to return a Swift Bool.
Build and...
Great. This works.
But I can make it a little better.
You see, even though I have this great new method for our Swift users, the Objective-C one is still available to them.
Swift clients might get confused and wonder which one they should use, so it'd be better to hide it from them.
But I don't want to stop Objective-C clients from using it and I don't want to totally block Swift either because this method I just wrote still needs to use it.
So, what I can do is... I go over to the SKMission header...
and I annotate the method with NS_REFINED_FOR_SWIFT.
What NS_REFINED_FOR_SWIFT does is very simple. It adds two underscores to the beginning of the method's Swift name.
When Xcode sees something with a leading underscore, it usually hides it from editor features like code completion and generated interfaces. So, if I build the project now, I'll get an error in my Swift code. Let's take a look at it.
Swift complains that save to doesn't have a wasDirty parameter.
This means the NS_REFINED_FOR_SWIFT macro is working. Swift doesn't think the method I'm trying to call is named save to. It now thinks it's underscore underscore save to.
And if I use code completion, there's no sign of the wasDirty version of the method there either.
But even though I can't see the method in the code completions, if I add two underscores before the name and build again...
it works. Now, our Swift clients will use this really nice wrapper that still calls the Objective-C method, but gives it an interface we couldn't have achieved from Objective-C alone.
You can use this technique whenever an API could be expressed more nicely in Swift. Next, let's look at a problem you might see in your framework. Swift goes to great lengths to import everything it can from your headers, but when it can't figure out how to import something, it'll skip over it and move on. This usually happens when there's no good or natural way to automatically translate an Objective-C feature into Swift. For example, Swift will skip over functions or methods which use C's variadic parameters and struct members which declare a C array of unknown size.
If a forward declaration, like an @class or @protocol with a semicolon, appears in a header file, but the class or protocol is never fully declared, Swift won't have enough information about that type to import it and it may end up dropping methods, properties or even entire categories that try to use one of these types.
If Swift sees two inconsistent declarations for the same thing, it will often skip over both of them rather than guess which one it should use.
In Xcode 12, Clang is now better at detecting these conflicts. So if you see types or methods suddenly disappear when you upgrade, that might be something to investigate.
And finally, Swift imports some macros, but not all. And this is where SpaceKit runs into trouble. Earlier, we turned a group of string constants into a type using NS_STRING_ENUM. SpaceKit actually has two sets of constants like this and we'd like to give the other set the same treatment. But these are defined with macros and there's a problem. One of them has been jettisoned.
What happened? Well, Swift can't import every macro you might write.
A macro is basically just a snippet of text that can be used anywhere in your Objective-C source code.
The same macro could mean different things when it's used in different places and there's no way for Swift to figure out how a macro was meant to be used. But Swift does recognize macros which match certain patterns that are often used to declare constants.
When it sees one of these, it imports it as a Swift constant. The first three SKCapsule macros fit one of the patterns Swift recognizes, a single string literal, so Swift imports them as constants.
But the fourth macro substitutes in another macro, then concatenates the string literal to it.
Swift doesn't fully understand how macro substitution could interact with other Objective-C features, so it allows you to name another macro or have other things in the macro, but not both. Since Swift doesn't recognize the pattern of code, it skips this macro and moves on.
There are a bunch of ways you could fix this unimportable macro. The simplest would be to just form the string literal in the same way as the others.
But if you want to turn these into string enum cases, it'd be better to convert them into real constants.
Then, you can string enum them, just like the SKRocket constants and be on your way.
Okay. So, at this point, you've strengthened your types, you fixed up some incorrect code and you've made sure everything that should be visible is.
Now comes the fun part. Polishing and improving the framework so it feels as nice as possible to Swift clients.
I'll start with some naming. Swift's method naming conventions are a little different from Objective-C's.
Both languages use relatively long names where every argument is labeled, but Swift's names tend to be a little shorter and omit information that's obvious from the types.
There's also a technical difference between the two languages. In Swift, each method has a base name and by default every argument also has a label.
Objective-C selectors essentially just have argument labels without a separate base name. So the information that would be in the base name is included in the first argument label. To help you with these differences, Swift automatically renames your Objective-C methods when it imports them. It strips off prefixes and suffixes that match type names and it uses a table of English grammar and vocabulary to figure out how to split the first portion of the selector into a base name and argument label. The results are usually pretty good, but this is basically a computer program making aesthetic judgments, so it'll sometimes make decisions you disagree with. For instance, many developers would say this method selector hasn't been split correctly. The word "flown" should be part of the argument label, not the base name, because the method fetches a list of previous missions and they are the missions flown by a particular astronaut.
Not every developer will agree, but it's a judgment call.
To fix it, you'd put the NS_SWIFT_NAME macro after the method, passing it the base name and argument labels the way you would write them in Swift if you were trying to refer to the method without calling it.
Swift will then import the method with the name you specified instead of the one it would generate itself. But NS_SWIFT_NAME is not just for methods. It can be applied to nearly anything.
For instance, take this enum.
Swift has already done a pretty good job with it. Because the author used NS_ENUM, it imports as a Swift enum.
But if we want to tweak the name a bit, NS_SWIFT_NAME can do that.
Now, you could use NS_SWIFT_NAME to remove the SK prefix from the name, but we don't actually recommend you do that.
A lot of Objective-C type names combine the framework's prefix with a word like "query" or "record" that would be too vague on its own.
You would need to add something else to the name to make up for the precision lost by deleting the prefix.
What it's best used for with types, though, is nesting them.
For instance, if there was an SKFuel class that this SKFuelKind enum went with, you could change it to be SKFuel.Kind, which is probably what you would call this type in Swift.
Another good use is with libraries whose type names look totally different from Swift types.
Like the lowercase type names you sometimes see in C libraries.
NS_SWIFT_NAME can also be used with global constants, variables and functions.
For example, we can apply NS_SWIFT_NAME to the SKFuelKind To String function to not only remove the extra information from its name, but also add an argument label, something Objective-C doesn't support on functions.
But now we've actually entered some really interesting territory because, when applied to global constants and variables, and especially global functions, NS_SWIFT_NAME gains some astonishing superpowers that can dramatically reshape how clients use your framework.
To start with, you can turn a global function into a static method by specifying the type's Objective-C name, followed by a dot and then the static method's name.
Then, you can turn it into an instance method instead by changing one of the argument labels to "self" so Swift knows where to pass the instance you called it on.
And then you can turn that method into a property by putting "getter" in front of it. You can do the same thing with a setter to create a mutable property.
Apply these techniques across an entire framework full of functions and you can dramatically reshape its whole API surface. If you've used Core Graphics in both Objective-C and Swift, you probably know what I mean. We applied this renaming capability to hundreds of global functions to convert them into methods, properties and initializers which are much easier to use.
Now, at this point, you must be wondering if there's anything NS_SWIFT_NAME can't do.
Well, there is.
Even though you renamed this global function into an instance property named "description," you can't use NS_SWIFT_NAME to conform the type to the CustomStringConvertible protocol, which would make Swift use that property to convert SKFuel.Kinds into strings. But you can add that conformance using one line of custom Swift code. So, let's do that.
The one line of code I need to add is this. I write an extension to SKFuel.Kind...
and conform it to the CustomStringConvertible protocol. And since my Objective-C header already provided a description property using NS_SWIFT_NAME...
That's it. There is no step three. I've only demonstrated very simple uses of custom Swift code, but you can write any Swift-only APIs you want in these Swift files.
For instance, you could import SwiftUI and write SwiftUI views that use AppKit or UIKit views from your framework. Or you could take an API that uses something like a completion handler and write a wrapper that returns a Combine Future instead. We can and have given entire sessions about using these technologies with Swift classes and types, and nothing really changes when you use them in a mixed-language framework. So, rather than try to give you a crash course, I'll point you to some relevant sessions on SwiftUI and Combine and leave the rest to you. Finally, let's talk about error code enums.
Like many frameworks, SpaceKit needs some custom error codes it can use with NSError.
To keep them from colliding with other frameworks' errors, it declares a string constant with an error domain and then an NS_ENUM with the specific error codes.
Now, looking at this generated interface, you might be thinking, "What's the problem here?' You declared a string constant and it came in as a string constant. You declared an NS_ENUM and it came in as a Swift enum with all the cases and codes intact and even the names correctly shortened. And really, there's no problem with the interface itself.
The problem becomes more clear when you think about how you would actually use it.
For instance, let's say you launch a mission and if the launch is aborted, you want to make sure the rescue squad goes and gets the astronauts. That's gonna look roughly like this. You call mission.launch, then you want to catch the error if it's a launch Aborted error.
But what does that catch clause actually look like? Well, we're going to need to extract the domain and code from the error, so first we have to catch it as an NSError.
Next, we need to make sure the error is in the SKErrorDomain, not some other domain that might use the same error code for something different.
Then we'll need to convert the error code from an int to an SKErrorCode. And finally, we can check if it's the case we wanted. Looking at all these checks together, this is, um...
not something I'd want to make my users write.
It shouldn't take this much effort just to match an error.
If you had written SpaceKit in Swift and this had been a Swift error enum, you could do the same thing in less than a line. Just name the error type and the case and Swift matches them against the thrown error. That's way better. But, of course, you don't have a Swift error enum. You have an Objective-C enum and an error domain constant.
Can you turn those into something similar to a Swift error enum? You can, and really it couldn't be easier. All you need to do is replace NS_ENUM with NS_ERROR_ENUM.
Then replace the raw type with the error domain constant.
This has an enormous effect on the error code enum.
SKErrorCode gets nested inside SKError, a new type Swift invents from whole cloth.
In the Xcode generated interface, you'll see that the enum has all of the error codes as its cases and they're also repeated as static constants on the struct, and there's a static constant for the domain, too.
But SKError also conforms to Error, so you can throw and catch it just like a native Swift error.
It has initializers and properties for the error code and user info dictionary.
And the error code enum has a tilde-equal operator.
This is the matching operator used by case and catch clauses. Here, it says that if you're switching or catching an error, you can match it against an SKErrorCode. The match succeeds only if the error domain and error code are both correct. Since SKError has those static code constants on it, you can write catch SKError.launch Aborted and Swift will use this operator to match the error against that error code. These parts of SKError aren't visible in the generated interface because the Swift compiler synthesizes them. But they're there for you to use and they make Objective-C error codes work pretty nicely in Swift. And the only thing you have to do to get all this is change two identifiers. Not bad for a one-line diff.
But I want to go back to where we started with these error codes and think about a bigger lesson.
There is no obvious sign of a problem in the generated interface.
It's only when you saw how SKErrorCode would be used that you realized there was room for improvement. All through this talk, I've been showing you generated interfaces because they're a good way to see how Swift is importing your headers. But generated interfaces are just a tool for understanding what's in the framework. What really matters is the calls your clients will write when they try to use it. So, when you're working on a framework, you should look at the generated interface, but you should think about the use sites. Imagine them in your mind's eye. Scribble them on a whiteboard. Tinker with them in a playground. Codify them in a test. Your clients will love or hate your framework based on the code they write with it, not the interface they see when they Command-click.
So, to sum up.
If you have an Objective-C framework, you can make it work really nicely for Swift clients with just header annotations and sometimes a little bit of Swift code.
When you're doing this, look for opportunities to present stronger, more specific types to Swift clients.
And make sure you're following Objective-C conventions so that Swift will use your framework correctly.
And even though Xcode shows you the generated interface, look beyond that and think about how your APIs will be used. That's what should drive your design.
If you want more information, the Swift documentation has a section called "Language Interoperability" which digs into all of these features and more in greater detail. If you want to get a more concrete feel for what makes a Swift API idiomatic, the "API Design Guidelines" describe some of the principles and rules recommended by the Swift core team and community. And the session "Behind the Scenes of the Xcode Build Process" digs deep into how Swift imports Objective-C headers and how Xcode builds mixed frameworks.
It's particularly relevant if Swift seems to be missing some of your Objective-C framework's headers or just won't import it at all.
So, thank you for your time and I hope this helps you launch something stellar.
-
-
4:43 - Describe nullability to control optionals (method and property annotations)
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> @interface SKMission : NSObject @property (readonly, nullable) NSString *name; - (nonnull instancetype)initWithName:(nullable NSString *)name; @end
-
6:53 - Describe nullability to control optionals (ASSUME_NONNULL blocks)
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject @property (readonly, nullable) NSString *name; - (instancetype)initWithName:(nullable NSString *)name; @end NS_ASSUME_NONNULL_END
-
7:14 - Describe nullability to control optionals (qualifiers)
// // Misc.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NSString * _Nonnull const SKRocketSaturnV; @interface ResourceValueContainer : NSObject - (BOOL)getResourceValue:(id _Nullable * _Nonnull)outValue error:(NSError**)error; @end
-
8:09 - Finding nullability mistakes with Objective-C tools
// // SKMission.h // #import <Foundation/Foundation.h> @interface SKMission : NSObject @property (strong, nonnull) NSString *rocket; @property (strong, nonnull) NSString *capsule; @end // // SKRocket.h // #import <Foundation/Foundation.h> extern NSString *_Nonnull const SKRocketSaturnV; // // SKMission.m // // Try building this file and then try analyzing it. // #import "SKRocket.h" #import "SKMission.h" @implementation SKMission @end @interface SKMissionConfigurator : NSObject @property (strong, nullable) SKMission *mission; @end @implementation SKMissionConfigurator - (void)testBadUseWithWarning { [self.mission setCapsule:nil]; } - (void)testBadUseWithStaticAnalyzer:(BOOL)missionIsSkylab1 { NSString *capsule = nil; if (!missionIsSkylab1) { capsule = SKCapsuleApolloCSM; } self.mission.capsule = capsule; } @end
-
11:07 - Use Objective-C generics for Foundation types
// // SKAstronaut.h // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKAstronaut : NSObject // Stub declaration @end NS_ASSUME_NONNULL_END // // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> #import <SpaceKit/SKAstronaut.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject @property (readonly) NSArray<SKAstronaut *> *crew; @end NS_ASSUME_NONNULL_END
-
11:33 - Use Int for numbers—unsigned types are for bitwise operations
// // SKRocket.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN NSInteger SKRocketStageCount(NSString *); NS_ASSUME_NONNULL_END // // NSData+xor.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> @interface NSData (xor) - (void)xorWithByte:(uint8_t)value; @end
-
13:23 - Strengthen stringly-typed constants
// // SKRocket.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef NSString *SKRocket NS_STRING_ENUM; extern SKRocket const SKRocketAtlas; extern SKRocket const SKRocketTitanII; extern SKRocket const SKRocketSaturnIB; extern SKRocket const SKRocketSaturnV; NSInteger SKRocketStageCount(SKRocket); NS_ASSUME_NONNULL_END
-
15:24 - Specify initializer behavior
// // SKAstronaut.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKAstronaut : NSObject - (instancetype)initWithNameComponents:(NSPersonNameComponents *)name NS_DESIGNATED_INITIALIZER; - (instancetype)initWithName:(NSString *)name; - (instancetype)init NS_UNAVAILABLE; @property (strong, readwrite) NSPersonNameComponents *nameComponents; @property (readonly) NSString *name; @end NS_ASSUME_NONNULL_END // // SKAstronaut.m // #import "SKAstronaut.h" @interface SKAstronaut () @property (class, readonly, strong) NSPersonNameComponentsFormatter *nameFormatter; @end @implementation SKAstronaut - (id)initWithNameComponents:(NSPersonNameComponents *)name { self = [super init]; if (self) { _name = name; } return self; } - (id)initWithName:(NSString *)name { return [self initWithNameComponents:[SKAstronaut _componentsFromName:name]]; } - (id)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (NSString *)name { return [SKAstronaut.nameFormatter stringFromPersonNameComponents:self.nameComponents]; } + (NSPersonNameComponents*)_componentsFromName:(NSString*)name { return [self.nameFormatter personNameComponentsFromString:name]; } + (NSPersonNameComponentsFormatter *)nameFormatter { static NSPersonNameComponentsFormatter *singleton; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ singleton = [NSPersonNameComponentsFormatter new]; }); return singleton; } @end
-
20:00 - Follow the error handling convention
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject /// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save; /// \c NO with nil \c *error` if nothing needed to be saved. - (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE; /// @param[out] wasDirty If provided, set to \c YES if the file needed to be /// saved or \c NO if there weren’t any changes to save. - (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error; @end NS_ASSUME_NONNULL_END
-
22:40 - Refine an Objective-C API for Swift users
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject /// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save; /// \c NO with nil \c *error` if nothing needed to be saved. - (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE; /// @param[out] wasDirty If provided, set to \c YES if the file needed to be /// saved or \c NO if there weren’t any changes to save. - (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error NS_REFINED_FOR_SWIFT; @end NS_ASSUME_NONNULL_END // // SwiftExtensions.swift // import Foundation extension SKMission { public func save(to url: URL) throws -> Bool { var wasDirty: ObjCBool = false try self.__save(to: url, wasDirty: &wasDirty) return wasDirty.boolValue } }
-
31:35 - Fix method names with NS_SWIFT_NAME
// // SKMission.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> #import <SKAstronaut/SKAstronaut.h> NS_ASSUME_NONNULL_BEGIN @interface SKMission : NSObject - (NSSet<SKMission *> *)previousMissionsFlownByAstronaut:(SKAstronaut *)astronaut NS_SWIFT_NAME(previousMissions(flownBy:)); @end
-
33:12 - Rename and rework value types with NS_SWIFT_NAME
// // SKFuelKind.h // // View the generated interface to see how Swift imports this header. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface SKFuel : NSObject // Stub class @end typedef NS_ENUM(NSInteger, SKFuelKind) { SKFuelKindH2 = 0, SKFuelKindCH4 = 1, SKFuelKindC12H26 = 2 } NS_SWIFT_NAME(SKFuel.Kind); NSString *SKFuelKindToNSString(SKFuelKind kind) NS_SWIFT_NAME(getter:SKFuelKind.description(self:));
-
35:59 - Add conformances to Objective-C types using custom Swift code
extension SKFuel.Kind: CustomStringConvertible {}
-
37:02 - Improve error code enums
// // SKError.h // SpaceKit // #import <Foundation/Foundation.h> extern NSString *const SKErrorDomain; typedef NS_ERROR_ENUM(SKErrorDomain, SKErrorCode) { SKErrorLaunchAborted = 1, SKErrorLaunchOutOfRange, SKErrorRapidUnscheduledDisassembly, SKErrorNotGoingToSpaceToday };
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.