Why does a Swift developer need a class name as a string?
\n\nImagine a situation: you are developing logging for a complex application. Each event should contain not only the message but also the name of the class in which it occurred. Or you are building a universal JSON parser that needs to determine the model type by a string key. Or you are debugging a retain cycle and want to dynamically print all objects in memory with their class names.
\n\nIn all these cases, you need the same thing: get the class name of an object in Swift as a string. It seems simple, right? But it's not that straightforward. In Swift, unlike Objective-C, there is no single "magic" method that works perfectly in all situations. Moreover, different approaches yield different performance, and some even produce unexpected results.
\n\nIn this article, we will not just give a ready answer, but analyze all the ways to get a class name in Swift, compare their speed, and show when to use which method. You will learn how to avoid pitfalls and write code that won't let you down in production.
\n\nThe classic way: String(describing: type(of:))
\n\nThe most popular answer on Stack Overflow for this topic is to use a combination of type(of:) and String(describing:). Let's break down how it works.
class Car {\n var model: String\n init(model: String) {\n self.model = model\n }\n}\n\nlet myCar = Car(model: "Tesla Model 3")\nlet className = String(describing: type(of: myCar))\nprint(className) // "Car"\n\nThis method is indeed simple and intuitive. type(of:) returns the metatype of the object, and String(describing:) converts it to a string. But there is a nuance: it does not work correctly for optional types and protocols.
Problem with optionals
\n\nlet optionalCar: Car? = Car(model: "BMW")\nlet optionalName = String(describing: type(of: optionalCar))\nprint(optionalName) // "Optional<Car>" — not what you expected!\n\nInstead of "Car", you get "Optional<Car>". This happens because type(of:) returns the type of the value itself, not its contents. For an optional, that is Optional<Car>.
Problem with protocols and inheritance
\n\nprotocol Drivable { }\nextension Car: Drivable { }\n\nlet drivableObject: Drivable = Car(model: "Audi")\nlet protocolName = String(describing: type(of: drivableObject))\nprint(protocolName) // "Car" — lucky here, but not always\n\nWith protocols, the result can be unpredictable, especially when using protocol composition or Any. Conclusion: this method is good for simple cases, but not for production code with optionals and complex types.
Objective-C legacy: NSStringFromClass
\n\nIf you come from Objective-C, you probably remember NSStringFromClass([self class]). This also works in Swift, but with caveats:
import Foundation\n\nclass Motorcycle {\n var brand: String\n init(brand: String) {\n self.brand = brand\n }\n}\n\nlet bike = Motorcycle(brand: "Harley")\nlet objcClassName = NSStringFromClass(type(of: bike))\nprint(objcClassName) // "_TtC11YourModule11Motorcycle" — mangled name!\n\nIn Swift, classes have "mangled" names. Instead of "Motorcycle", you get something like _TtC11YourModule11Motorcycle. To get the clean name, you need to "demangle" it manually or use String(describing:).
However, there is an important advantage: NSStringFromClass works with any objects, including optionals, and does not crash on nil. But for code clarity, it's better not to use it directly.
When is NSStringFromClass still needed?
\n- \n
- When working with Obje