📜 ⬆️ ⬇️

Objective-C compliant "Swift" code

Although Apple has written, it would seem, detailed documentation on how you can use the Swift code inside the Objective-C application (and vice versa), but when it comes to business, this is for some reason not enough. When I first came up with the need to ensure compatibility of the framework written entirely in Swift with an Objective-C application, the Apple documentation for some reason gave rise to more questions than it gave answers (or at least left many spaces) . Intensive use of search engines showed that this topic was covered in the Web quite poorly: a couple of questions on StackOverflow , a couple of introductory articles (on English-language resources, of course) - that's all that was found.

This article is a summary of the information found, as well as the experience gained. I emphasize that she does not claim to be called, as they say, good practice, but only suggests possible actions in the circumstances described or is a kind of academic experiment.


')
Last updated February 2019.

TL; DR. To use the “Swift” code inside the “Objective-C” you will have to sacrifice some “features” of the “Swift” and write a wrapper over the code that will not use techniques that are incompatible with the “Objective-C” ( “structures” , “generics” , “Enum associated values” , “protocol extensions” , etc.), and will be based on the NSObject heirs .


Start



So, we have an Objective-C project and some Swift code that we want to use in this project. For example, let it be a third-party “Swift” framework, which we add to the project, say, with the help of CocoaPods . As usual, add the desired dependency in the “Podfile” , execute pod install , open the “xcworkspace” file .

To import a framework into an Objective-C file, you do not need to import entire framework, as we used to do in Swift, nor try to import individual framework API public files, as we used to do in Objective-C. In any file in which we need access to the framework's functionality, we import a file called <>-Swift.h - this is an automatically generated header file that is a guide to “Objective-C” files to the public “API” contained in imported “Swift” files. It looks like this:

 #import "YourProjectName-Swift.h" 


Using Swift Classes in Objective-C Files



If you succeeded after importing the “Swift” heading, just using any of its classes or methods in the “Objective-C” project, you are very lucky - it means that someone before you takes care of compatibility. The fact is that Objective-C “digests” only the descendant classes of NSObject and sees only the public “API”. And inside classes, public properties , initializers, and methods must be annotated with @objc .

If we import our own “Swift” code, then we, of course, have the opportunity to add the “inheritance” from anything, and add an annotation (or attribute) @objc . But in this case, probably, we have the opportunity and the necessary code to write in “Objective-C”. Therefore, it makes more sense to focus on the case when we want to import someone else's “Swift” code into our project. In this case, most likely, we have no possibility to add any inheritance or anything to the necessary classes. What to do in this case? It remains to write wrappers !

Suppose the imported framework contains the following class we need:

 public class SwiftClass { public func swiftMethod() { // Implementation goes here. } } 


We create our “Swift” file, import the external framework into it, create our own class inherited from NSObject , and in it declare a private member of the external class type. To be able to call the methods of the outer class, we define methods in our class that internally call the corresponding methods of the outer class through a private member of the class (it sounds confusing, but I think everything’s clear by code):

 import Foundation import SwiftFramework public class SwiftClassObjCWrapper: NSObject { private let swiftClass = SwiftClass() public func swiftMethod() { swiftClass.swiftMethod() } } 


(Access to the NSObject class and NSObject annotations appears after importing Foundation .)

For obvious reasons, we cannot use the same class and method names in declarations. And here the annotation @objc comes to the rescue:

 @objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { private let swiftClass = SwiftClass() @objc public func swiftMethod() { swiftClass.swiftMethod() } } 


Now, when calling from the Objective-C code, the names of the classes and methods will look exactly as we would like them to be - as if we are writing the corresponding names from the external class:

 SwiftClass *swiftClass = [SwiftClass new]; [swiftClass swiftMethod]; 


Features of using “Swift” methods in “Objective-C” files



Unfortunately, not any (public) “Swift” methods can simply be marked @objc and used inside Objective-C. Swift and Objective-C are different languages ​​with different capabilities and different logic, and quite often when writing a Swift code, we use its capabilities that Objective-C does not have or that are implemented fundamentally differently.

For example, the default settings will have to be abandoned. This method:

 @objc public func anotherSwiftMethod(parameter: Int = 1) { // Implementation goes here. } 


... inside the "Objective-C" code will look like this:

 [swiftClassObject anotherSwiftMethodWithParameter:1]; 


( 1 is the value passed by us, the argument has no default value.)

Method names



“Objective-C” has its own system, according to which the “Swift” method will be named in the “Objective-C” environment. In most simple cases, it is quite satisfactory, but often requires our intervention to become readable. For example, the method name in the spirit of do(thing:) “Objective-C” will transform into doWithThing: which may not coincide with our intention. In this case, the @objc annotation @objc comes to the rescue:

 @objc(doThing:) public func do(thing: Type) { // Implementation goes here. } 


Methods throwing exceptions



If the “Swift” method is marked with throws , then “Objective-C” will add one more parameter to its signature — an error that the method may throw. For example:

 @objc(doThing:error:) public func do(thing: Type) throws { // Implementation goes here. } 


Using this method will occur in the spirit of "Objective-C" (so to speak):

 NSError *error = nil; [swiftClassObject doThing:thingValue error:&error]; if (error != nil) { // Handle error. } 


Using Swift Types in Parameters and Return Values



If the parameter values ​​or the return value of “Swift” - a non-standard “Swift” type is used that is not transferred automatically to the Objective-C environment, this method will not be used in the Objective-C environment again ... over him not to "conjure".

If this “Swift” -type is an inheritor of NSObject , then, as mentioned above, there are no problems. But most often it turns out that it is not. In this case, the wrapper helps us out again. For example, the original "Swift" code:

 class SwiftClass { func swiftMethod() { // } } class AnotherSwiftClass { func anotherSwiftMethod() -> SwiftClass { return SwiftClass() } } 


Wrap for him:

 @objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { private let swiftClassObject: SwiftClass init(swiftClassObject: SwiftClass) { self.swiftClassObject = swiftClassObject super.init() } @objc public func swiftMethod() { swiftClassObject.swiftMethod() } } @objc(AnotherSwiftClass) public class AnotherSwiftClassWrapper: NSObject { private let anotherSwiftClassObject = AnotherSwiftClass() @objc func anotherSwiftMethod() -> SwiftClassObjCWrapper { return SwiftClassObjCWrapper(swiftClassObject: anotherSwiftClassObject.anotherSwiftMethod()) } } 


Using inside Objective-C:

 AnotherSwiftClass *anotherSwiftClassObject = [AnotherSwiftClass new]; SwiftClass *swiftClassObject = [anotherSwiftClassObject anotherSwiftMethod]; [swiftClassObject swiftMethod]; 


Implementing Swift Protocols Objective-C Classes



For example, let's take, of course, the protocol, in the parameters or return values ​​of methods of which Swift-types are used, which cannot be used in Objective-C:

 public class SwiftClass { } public protocol SwiftProtocol { func swiftProtocolMethod() -> SwiftClass } public func swiftMethod(swiftProtocolObject: SwiftProtocol) { // Implementation goes here. } 


We'll have to wrap up again. For starters, SwiftClass :

 @objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { let swiftClassObject = SwiftClass() } 


Next, we will write our own protocol, similar to SwiftProtocol , but using wrapped versions of the classes:

 @objc(SwiftProtocol) public protocol SwiftProtocolObjCWrapper { func swiftProtocolMethod() -> SwiftClassObjCWrapper } 


Then the most interesting thing: we will declare the “Swift” class, which adapts the “Swift” protocol we need. It will be something of a bridge between our protocol, which we wrote to adapt in the “Objective-C” project and the “Swift” method, which accepts the object of the original “Swift” protocol. The class members will be listed as an instance of the protocol that we described. And the class methods in the protocol methods will call the methods of the protocol we wrote:

 class SwiftProtocolWrapper: SwiftProtocol { private let swiftProtocolObject: SwiftProtocolObjCWrapper init(swiftProtocolObject: SwiftProtocolObjCWrapper) { self.swiftProtocolObject = swiftProtocolObject } func swiftProtocolMethod() -> SwiftClass { return swiftProtocolObject.swiftProtocolMethod().swiftClassObject } } 


Unfortunately, wrapping a method that accepts a protocol instance cannot be done:

 @objc public func swiftMethodWith(swiftProtocolObject: SwiftProtocolObjCWrapper) { methodOwnerObject.swiftMethodWith(swiftProtocolObject: SwiftProtocolWrapper(swiftProtocolObject: swiftProtocolObject)) } 


Not the easiest chain? Yes. Although, if the classes and protocols used have a significant number of methods, the wrapper will not seem so disproportionately voluminous with respect to the source code.

Actually, using the protocol in the “Objective-C” code itself will already look quite harmonious. Implementation of protocol methods:

 @interface ObjectiveCClass: NSObject<SwiftProtocol> @end @implementation ObjectiveCClass - (SwiftClass *)swiftProtocolMethod { return [SwiftClass new]; } @end 


And using the method:

 (ObjectiveCClass *)objectiveCClassObject = [ObjectiveCClass new]; [methodOwnerObject swiftMethodWithSwiftProtocolObject:objectiveCClassObject]; 


Enumerated types in "Swift" and "Objective-C"



When using the “Swift” enumerated types in the “Objective-C” projects, there is only one nuance: they must have an integer Raw Type . Only then can we annotate enum as @objc .

What to do if we cannot change the enum type, but want to use it inside „Objective-C“? We can, as usual, wrap a method that uses instances of this enum type, and give it our own enum . For example:

 enum SwiftEnum { case firstCase case secondCase } class SwiftClass { func swiftMethod() -> SwiftEnum { // Implementation goes here. } } @objc(SwiftEnum) enum SwiftEnumObjCWrapper: Int { case firstCase case secondCase } @objc(SwiftClass) public class SwiftClassObjCWrapper: NSObject { let swiftClassObject = SwiftClass() @objc public func swiftMethod() -> SwiftEnumObjCWrapper { switch swiftClassObject.swiftMethod() { case .firstCase: return .firstCase case .secondCase: return .secondCase } } } 


Conclusion



Here, perhaps, all that I wanted to report on this topic. Most likely, there are other aspects of integration of the “Swift” code in “Objective-C”, but I am sure that it is quite possible to cope with them by armed with the logic described above.

This approach, of course, has its drawbacks. In addition to the most obvious (writing a significant amount of additional code), there is one more important one: the “Swift” code is transferred to the “Objective-C” runtime and will most likely not work as fast or, at least, otherwise. Although the difference in many cases with the naked eye will not be noticeable.

Source: https://habr.com/ru/post/345946/


All Articles