how to view the contents of any entity

Swift, like many other programming languages, has the ability to obtain information about the structure of an object at Runtime. For this purpose, the language has a special mechanism – Reflection. With its help, you can view the contents of any entities without knowing absolutely nothing about them.

My name is Svetlana Gladysheva, I am an iOS developer at Tinkoff. I’ll tell you what capabilities Reflection has in Swift, what its limitations and pitfalls are. Let's look at its application with examples and find out why it can be used in everyday work. We’ll also talk about how you can disable Reflection in a project and what it can affect.

View different types of content

To view the contents of entities in Swift, a special structure is used – Mirror. You can use Mirror for any type: structures, classes, enumerations, collections, and so on. You can create a Mirror like this:

let mirror = Mirror(reflecting: item)

Mirror has a Children field that allows you to view the contents of an entity. Children is a collection consisting of Child elements. Each Child has a Label, the name of the element, and a Value, the value it contains. With Children, you can view different types of content.

For structures. Let's say we have a User structure and an instance of this structure:

struct User {
    let name: String
    let age: Int
}

let user = User(name: "Nikita", age: 25)

We can see what is inside the User using a special structure – Mirror:

let mirror = Mirror(reflecting: user)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

The console will display:

We learned that inside User there is a Name field with the value Nikita and an Age field with the value 25. It is important that we can only look at fields, but not object methods. But we can see all the fields – both public and private.

For classes. Let's say we have a Validator class with two private fields:

class Validator {
    private let mode: ValidatorMode
    private let transformer: Transformer
    // …
}

ValidatorMode is an Enum with two options: Simple and Complex, and Transformer is some other class. We can display information about an object of this class:

let mirror = Mirror(reflecting: validator)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

The console will display:

Just like for structures, we cannot see the methods of objects, but we see the contents of all fields. If there is some other object inside the object, only the type of the object is displayed, but not the contents. If you need to find out what's inside such an object, you can create a Mirror for this object and view its Children.

For Enum. Let's say there is an Enum ValidationType with two options:

enum ValidationType {
    case email
    case phoneNumber(format: String, city: String)
}

Let's check the code for the Email case:

let mirror = Mirror(reflecting: ValidationType.email)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

Nothing will be printed to the console. Because for Enum as Children, Mirror has Associated Values. The Email case does not have a single Associated Value, so the Children collection is empty for it.

The PhoneNumber case has two Associated Values: Format and City. If we run the same code for this case, the following line will be displayed in the console:

The Label for Enum is the name of the case, and the Value is a Tuple containing all Associated Values ​​in the “name: value” format. If the case has only one parameter, then it is displayed in Value, without a name.

Other types. For an array, you can view its elements: Label will be Nil, and Value will be the values ​​of the array elements. For Dictionary, the Label will be the keys, and the Value will be the values. As an example, I listed only two types, but this way you can view everything.

Different types of fields. Static and computed fields cannot be viewed using Mirror, but the values ​​of Lazy fields can be viewed. Let's say some class has a lazy Transformer field:

private lazy var transformer: Transformer = Transformer()

If we write like this:

mirror.superclassMirror?.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

The console will display:

To the lazy fields in child.name the prefix 'lazy_storage' is added.

View object types and more

Using Mirror, you can find out not only the contents of an object, but also its type. For this purpose, Mirror has a subjectType. For the User structure from the previous section, subjectType will return the User type, and for the Validator class, the Validator type.

Mirror also has a displayType – a field that shows exactly how this Mirror will be displayed. DiplayStyle is an Enum that can take the value Struct, Class, Enum, Tuple, Optional, Collection, Dictionary, Set and so on. Using displayType, we can find out exactly how an entity will be displayed: as a class, as a structure, or as something else.

If a class inherits from another class, Children will show us only those fields that are declared inside this class itself. If we want to see the values ​​of the fields that are declared in the base class, we can do this using SuperclassMirror:

mirror.superclassMirror?.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

It may happen that a base class has its own base class. You can get its fields using the construction mirror.superclassMirror?.superclassMirror?.children.

To get absolutely all the fields of a class, you can write the code:

var mirror: Mirror? = Mirror(reflecting: item)
repeat {
    mirror?.children.forEach { child in
        print("Label: \(child.label), value: \(child.value)")
    }
    mirror = mirror?.superclassMirror
} while mirror != nil

Search using the Descendant method. It is not necessary to write some complex code when you need to find a specific field on an object. Mirror has a Descendant method that allows you to get the value of a field using MirrorPath. As MirrorPath, you can pass either a string – the name of the field, or Int – the serial number of the field.

Let's assume there are two structures:

struct User {
    let name: Name
    let age: Int
}

struct Name {
    let firstName: String
    let secondName: String
}

Having created a Mirror for the User instance, we can get the SecondName value using the Descendant method:

let secondName = mirror.descendant("name", "secondName")

Or using the serial numbers of the fields we need:

let secondName = mirror.descendant(0, 1)

Changing the representation of an object

Swift has the CustomReflectable protocol, which allows you to replace a mirror object with a custom one. They do it if the object is Mirror for some reason I'm not happy with it.

You need to make the entity comply with the CustomReflectable protocol in order to change the representation of it. For CustomMirror, your own Mirror object is created and Children are specified in it with a dictionary of the form [“label”: value].

For example, let's take the Point structure:

struct Point {
    let x: Int
    let y: Int
}

We can make this structure follow the CustomReflectable protocol and add CustomMirror to it:

extension Point: CustomReflectable {
    var customMirror: Mirror {
        Mirror(self, children: ["z": x + y])
    }
}

Let's create and display information about some instance of this structure to the console:

let point = Point(x: 10, y: 12)
let mirror = Mirror(reflecting: point)
mirror.children.forEach { child in
    print("Label: \(child.label), value: \(child.value)")
}

Result in the console:

Exactly what was specified in CustomMirror was displayed in the console. At the same time, we did not receive information about the real fields of the structure x and y.

When calling Mirror(reflecting: point) for a type that complies with the CustomReflectable protocol, CustomMirror from CustomReflectable will be used. So, using Reflection in Swift, we can receive not entirely true information about the fields an object has.

You can specify a DisplayStyle and AncestorRepresentation when creating a CustomMirror. Using AncestorRepresentation, you can create a CustomMirror that will be rendered as a SuperclassMirror. You can also use it to prevent this Mirror from displaying information about superclasses.

Examples of using Reflection

With validation. Let's say we have an Item class that contains many fields of type String:

struct Item {
    let field1: String
    let field2: String
    let field3: String
    let field4: String
    // …
}

We want to check that all the fields in the Item object are not empty, that is, none of the fields have an empty string. Let's write a validator for this:

class ItemValidator {
    func validate(item: Item) -> Bool {
        return !item.field1.isEmpty && !item.field2.isEmpty && !item.field3.isEmpty && !item.field4.isEmpty // …
    }
}

If Item has a lot of fields, the check inside the Validate method will be long and cumbersome. Moreover, when adding a new field to Item, you can forget about the need to edit the Validate method, and then the validator will not work correctly.

Let's rewrite the Validate method using Reflection:

func vaildate(item: Item) -> Bool {
    let mirror = Mirror(reflecting: item)
    for child in mirror.children {
        if let stringValue = child.value as? String {
            if stringValue.isEmpty {
                return false
            }
        }
    }
    return true
}

Now there is no long check in the Validate method, and when adding a new field to Item, you will not need to change this method.

The method rewriting approach also has its downsides. This check is not obvious to other developers. Therefore, another developer, not knowing about the use of Reflection in the validation method, may make changes that will lead to incorrect operation of the program. For example, he can add a new field to Item that does not need to be validated, but it will be validated in the Validate method.

With tests. Let's say we have a MainViewConroller that has a saveButton:

class MainViewConroller: UIViewController {

    private let saveButton: UIButton

    // ...

    func updateView() {
        // ...
        saveButton.isEnabled = false
    }
}

Let's write a test for the updateView method, in which we will check that the button becomes disabled:

func testUpdateView() {
    let mainViewContoller = MainViewConroller()
    mainViewContoller.updateView()

    XCTAssertFalse(mainViewConroller.saveButton.isEnabled)
}

But this code won't compile because the saveButton field is private. You can make saveButton non-private, and then the test will work, but making fields public only for tests is not the best solution.

Let's write a method that will receive the button we need using Reflection:

func getSaveButton(from mainViewConroller: MainViewConroller) -> UIButton? {
    let mirror = Mirror(reflecting: mainViewConroller)
    if let saveButton = mirror.descendant("saveButton") as? UIButton?  {
        return saveButton
    }
    return nil
}

Let's rewrite the test using the written method:

func testUpdateView() {
    let mainViewContoller = MainViewConroller()
    mainViewContoller.updateView()

    let saveButton = getSaveButton(from: mainViewContoller)
    XCTAssertEqual(saveButton?.isEnabled, false)
}

We were able to write a test while leaving the button private.

Other examples of use. Reflection can be useful in cases where you need to find out the type of an object, the value of private fields, or access all fields of an object.

With Reflection, you can learn how objects from linked libraries are constructed to better understand how they work. For example, you can see what private fields objects from Foundation, UIKit or SwiftUI have.

Disabling Reflection

To disable Reflection, you need to change the Reflection Metadata Level value in the project settings. You can change the value of the Reflection Metadata Level item in Build Settings or in the file plist.info specify the desired value for SWIFT_REFLECTION_METADATA_LEVEL.

There are three levels in Reflection Metadata Level:

All - all inclusive.  Without Names - field names will not be displayed, Label values ​​will always be Nil.  None — Reflection is completely disabled

All – all inclusive. Without Names – field names will not be displayed, Label values ​​will always be Nil. None — Reflection is completely disabled

You can disable Reflection using the -disable-reflection-metadata and -disable-reflection-names flags.

When disabling Reflection, it is important to know that the debugger uses this mechanism for its work. For example, the Dump function uses Reflection to display the contents of entities. LLDB uses Reflection Metadata to obtain type information. Memory Graph Debugger also uses Reflection Metadata for its needs. Because of this, if Reflection is disabled, all of the listed tools may stop working correctly.

Disabling Reflection may have other consequences. For example, it stops working correctly @Published in SwiftUI. The description of the object changes when used String(reflecting: ...) And String(describing: ...). Interpolation of Enum values ​​stops working correctly: when trying to print a value to the console using the construct print("\(someEnum)") Instead of the case name, we will get a result like “SomeEnum(rawValue: 2131241)”.

Fortunately, you can set different Reflection Metadata Levels for different targets in a project. For example, you can enable Reflection in the test target, but turn it off in the main target. It is possible to enable or disable Reflection for different modules in a multi-module application.

The Reflection Metadata Level setting contains the word metadata because Swift Runtime stores metadata for each type used in the program. They contain useful information about this type. For example, for classes and structures, you can find field names and their types in their metadata. From this metadata, Reflection receives all the necessary information, with the help of which the contents of objects are determined.

Conclusion

Reflection in Swift works as read-only. It is impossible to change entities in Runtime – you can only view their contents. Compared to other programming languages, such as Java, Reflection's capabilities seem severely limited. But the restriction was introduced intentionally, since it was important to the developers of the Swift language that the language be safe.

Using Reflection, you can find out the type of entities and read the values ​​of private variables. But it's important to understand that using Reflection may not be obvious to other developers. Private fields belong to the internal implementation of classes and structures, so they can change frequently. In addition, Reflection may be disabled for some reason, and then code based on this mechanism will stop working. Therefore, everyone decides for themselves whether to use Reflection in their project.

A couple of useful links to say goodbye:

An article on swift.org about how Mirror works. Although the material is quite old, it can help to look inside and understand how Reflection was implemented.

More information about metadata can be found on GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *