Print property name of protocol

Hello!

I want to print some property names of a protocol in a type safe way. One way I have found is using a struct conforming to the protocol and then using key paths. However, is there also another way where I can directly use the property and print it?

protocol A {
   var myName: Int { get }
}

print(....) // Result -> "myName"

I don’t know of a built-in way, but it doesn’t sound hard to write a macro equivalent to C# nameof. Unfortunately, that would mean setting up a macro library.

1 Like

Indeed! In fact, it's the example given by the Swift Package Manager's macro template. They call it #stringify, and it could be used like:

protocol A {
   var myName: Int { get }
}

extension A {
    func demo() {
        let _, propertyName = #stringify(myName)
        print(propertyName)
    }
}

It needs to be called within the scope of the A protocol though, since the contents inside the #stringify(...) needs to be valid Swift code. So:

  1. myName or self.myName inside those parens works,
  2. #stringify(A.myName) doesn't work, since there's no static property with that name on A
  3. #stringify(myName) outside that scope doesn't work, since that would be referring to a global variable myName, and not the protocol's instanced property myName.
3 Likes

Inefficient, hacky and subject to break, but this works (for now)

protocol A {
   var myName: Int { get }
}

func nameof<Receiver, _PropertyType>(_ keypath: KeyPath<Receiver, _PropertyType>) -> String {
   let typeName = String(describing: Receiver.self)
   print(typeName)
   
   let kpDesc = keypath.debugDescription
   let expectedPrefix = #"\\#(typeName)."#
   
   guard kpDesc.hasPrefix(expectedPrefix) else {
      // FIXME: The formatting of KeyPath.debugDescription changed... Handle this error, somehow.
      return "Welp, expected \(kpDesc) to start with \(expectedPrefix), but it didn't."
   }
   
   return String(kpDesc.dropFirst(expectedPrefix.count))
}

let propertyName = nameof(\A.myName)
print(propertyName)
3 Likes

You can try using Mirror.

protocol MyProtocol {
    var name: String { get }
    var age: Int { get }
}

struct Person: MyProtocol {
    var name: String
    var age: Int
    var address: String
}

func getProtocolPropertyNames<T: MyProtocol>(_ object: T) -> [String] {
    let mirror = Mirror(reflecting: object)
    var propertyNames: [String] = []
    
    for child in mirror.children {
        if let propertyName = child.label {
            propertyNames.append(propertyName)
        }
    }
    return propertyNames
}

let person = Person(name: "Alice", age: 30, address: "Wonderland")
let properties = getProtocolPropertyNames(person)
print(properties) //  ["name", "age", "address"]
1 Like