Extracting the type or class name of an object as a string is a standard requirement in Swift development. It commonly powers logging subsystems, analytics event tagging, dependency injection containers, and dynamic table or collection view cell reuse identifiers. However, using legacy Objective-C runtime functions often results in mangled symbols or unwanted module prefixes.
Here is how to retrieve clean, demangled class and structure names in Swift across instances and static types.
Why this happens
When developers transition from Objective-C or use legacy runtime calls like object_getClassName(myObject) or NSStringFromClass(...), the returned string looks like _TtC9MyProject22CalendarViewController or MyProject.CalendarViewController.
This behavior exists because Swift uses name mangling and namespacing to prevent symbol collisions:
- Namespacing by Module: Swift classes belong to modules. Under the hood, an Objective-C-compatible Swift class is registered with the Objective-C runtime as
ModuleName.ClassNameto avoid clashing with identically named classes in other frameworks. - Swift Symbol Mangling: Raw runtime functions inspect the low-level metadata tables. If the class does not explicitly declare
@objc(CustomName), Swift encodes metadata symbols with prefixes like_TtCfollowed by string lengths and identifiers so the runtime linker can uniquely resolve dynamic types.
Directly calling low-level C runtime utilities bypasses Swift’s built-in reflection facilities, giving you the raw mangled string instead of the human-readable class name you declared in source code.
How to fix it
1. Use String(describing: type(of: instance))
The idiomatic and modern Swift method for resolving an instance’s type at runtime is combining type(of:) with String(describing:). This works reliably for classes, structs, enums, and protocols.
import UIKit
class ProfileViewController: UIViewController {}
let controller = ProfileViewController()
// Dynamic type from instance
let className = String(describing: type(of: controller))
print(className) // Output: "ProfileViewController"
type(of:) evaluates the dynamic runtime type rather than the static variable type. If you have an instance stored as a superclass type, type(of:) accurately reflects the concrete subclass:
let baseController: UIViewController = ProfileViewController()
print(String(describing: type(of: baseController))) // Output: "ProfileViewController"
2. Extract Names from Metatypes Directly
If you do not have an active instance and want to obtain the type name from a class, struct, or enum directly, reference .self:
struct UserProfile {
let id: String
}
let structName = String(describing: UserProfile.self)
print(structName) // Output: "UserProfile"
This approach avoids instantiating heavyweight classes simply to inspect their identifiers.
3. Create a Reusable Identifiable Protocol
Hardcoding String(describing:) throughout view controllers or cell registration leads to boilerplate. You can define a protocol with a default implementation that exposes both instance and static properties:
import Foundation
protocol NameDescribable {
static var typeName: String { get }
var typeName: String { get }
}
extension NameDescribable {
static var typeName: String {
return String(describing: self)
}
var typeName: String {
return String(describing: type(of: self))
}
}
Adopt this protocol across any structure or class:
class SettingsCoordinator: NameDescribable {}
// Using the static property
print(SettingsCoordinator.typeName) // Output: "SettingsCoordinator"
// Using the instance property
let coordinator = SettingsCoordinator()
print(coordinator.typeName) // Output: "SettingsCoordinator"
4. Apply Type Names for UIKit Cell Reuse Identifiers
A primary use case for type names is registering and dequeuing UITableViewCell and UICollectionViewCell instances without hardcoded magic strings.
import UIKit
protocol ReusableView: AnyObject {
static var defaultReuseIdentifier: String { get }
}
extension ReusableView where Self: UIView {
static var defaultReuseIdentifier: String {
return String(describing: self)
}
}
extension UITableViewCell: ReusableView {}
// Implementation in a custom cell
class ProductCardCell: UITableViewCell {
// ProductCardCell.defaultReuseIdentifier is automatically "ProductCardCell"
}
// Usage in Table View setup
func setupTableView(tableView: UITableView) {
tableView.register(
ProductCardCell.self,
forCellReuseIdentifier: ProductCardCell.defaultReuseIdentifier
)
}
5. Stripping Module Names from NSStringFromClass
If you work extensively with legacy Objective-C APIs or need to support NSObject subclasses specifically, NSStringFromClass paired with string splitting is an alternative:
import Foundation
extension NSObject {
var pureClassName: String {
return NSStringFromClass(type(of: self))
.components(separatedBy: ".")
.last ?? String(describing: type(of: self))
}
}
class InvoiceHeaderView: NSObject {}
let view = InvoiceHeaderView()
print(view.pureClassName) // Output: "InvoiceHeaderView"
While functional, String(describing: type(of: self)) is preferred because it works seamlessly on pure Swift structs, generic types, and enums without importing the Objective-C runtime.
How to prevent it
- Avoid runtime C helpers: Refrain from using
object_getClassName()in Swift code. It returns internal runtime symbol strings that change with compilation flags and module structures. - Beware of generic type formatting: Be aware that calling
String(describing: NetworkResponse<User>.self)outputs"NetworkResponse<User>". If you need solely the outer base name, parse the string before the opening angle bracket or define a custom mapping. - Do not use class names for persistence keys: Never rely on dynamic type names for Core Data models, Codable persistence keys, or Keychain identifiers. Renaming a class during refactoring will break existing stored data.
- Guard against minification and obfuscation: If your project integrates third-party code obfuscators or symbol strippers, runtime class names may be converted into non-descriptive tokens.
FAQ
Does String(describing:) work for Swift value types like structs and enums?
Yes. Unlike NSStringFromClass or object_getClassName, String(describing:) is part of the Swift Standard Library and fully supports structs, enums, tuples, and custom classes.
What does String(describing:) return for generic classes?
It returns the base name accompanied by its generic parameter types, such as "Repository<Order>". If you require only the base class name, you must split the string at the < character.
Can I use the class name string to instantiate an object dynamically?
To instantiate an object from a string in Swift, the class must inherit from NSObject and be resolved using NSClassFromString("ModuleName.ClassName") as? NSObject.Type. Swift does not provide arbitrary dynamic instantiation for pure Swift types by string name alone.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
