Published: Oct 6, 2015
What happens when an Objective-C app includes two or more Swift frameworks that happen to have a conflicting class name? Frustration.
Swift rightfully doesn’t encourage the use of namespace prefixes, meaning this frustration will become increasingly common as more Swift frameworks are open-sourced and used in Objective-C apps.
Let’s see a concrete example of the problem and how we can minimize its likelihood.
#
The problemConsider the following pair of frameworks:
// Within Framework1.framework @objc public class SomeManager: NSObject { public func doThing() { NSLog(@"SomeManager - Swift - Framework1"); } } // Within Framework2.framework @objc public class SomeManager: NSObject { public func doThing() { NSLog(@"SomeManager - Swift - Framework2"); } }
We might find the following code in an app that includes both frameworks:
SomeManager *instance = [SomeManager new]; [instance doThing];
Can you guess which class is instantiated? Not likely, because the answer is undefined. The linker will map an arbitrary implementation to the class each time the app is rebuilt.
#
Namespace prefixingBarring an Objective-C language feature that allows for the distinction of symbols by framework, we need to do the next-best thing: namespace prefixing.
Prefixing Objective-C classes is such a common practice that it’s automatic for most Objective-C developers. Software designers new to programming and learning Swift may not be familiar with this however, so in short:
Namespace prefixing is the practice of adding a three or four letter prefix (Apple has reserved two-letter prefixes) to the beginning of a symbol exposed by your framework. For example: if your company’s name was Lumi you might use LUMI as your prefix.
LUMISomeClass
,LUMISomeFunc
, orLUMISomeStruct
would all be examples of symbol names in your framework.
Swift rightfully doesn’t encourage the use of namespace prefixes because frameworks are the namespace. But Objective-C will treat Swift frameworks like any old Objective-C framework and so that namespacing is lost. Thankfully, Swift provides @objc(name)
.
@objc(name)
allows you to provide an Objective-C-specific name for a class to be used only from Objective-C code. If we modify our original example:
// Within Framework1.framework @objc(FW1SomeManager) public class SomeManager: NSObject { public func doThing() { NSLog(@"SomeManager - Swift - Framework1"); } } // Within Framework2.framework @objc(FW2SomeManager) public class SomeManager: NSObject { public func doThing() { NSLog(@"SomeManager - Swift - Framework2"); } }
These classes would be referred to in Objective-C as FW1SomeManager
and FW2SomeManager
while Swift code could still use SomeManager
and Framework1.SomeManager
as before - win win!
#
Concluding notesWhen writing Swift frameworks that may used by Objective-C apps, prefix your Swift class and struct names using @objc()
. Not doing so in a shared or open source framework can lead to undefined behavior in the event of naming conflicts.
Note that @objc()
not presently allow you to provide an Objective-C name for enum
types.
#
llvm feature requestIn the absence of prefixed Swift symbols, a linker warning would be a helpful tool for identifying situations where the linker had to choose between duplicate symbols. Sadly, I could not find any such linker warning. This would be a welcome addition to llvm so I’ve filed a feature request.
In exploring solutions to this problem I found out that you can refer to a specific Swift framework class using NSClassFromString.
Disclaimer: I do not encourage the use of this fact in a production application.
For example, NSClassFromString(@"Framework2.SomeManager")
allows you to instantiate Framework2’s SomeManager. This approach can’t be checked at compile-time and may be subject to changes in future Swift releases, so its utility is limited. This does not work for Objective-C classes contained within frameworks.
#
References@featherless: You can namespace classes in Objective-C only with @ objc(ABCMyClass)
— Scott Berrevoets (@ScottBerrevoets) October 6, 2015