Can the non-string "property name" passed to #keyPath() be saved independently?
Asked Answered
E

2

8

I was very happy to find Swift 3's implementation of #keyPath(), which will eliminate typos and enforce at compile time that the key path actually exists. Much better than manually typing Strings.

https://github.com/apple/swift-evolution/blob/master/proposals/0062-objc-keypaths.md

class MyObject {
    @objc var myString: String = "default"
}

// Works great
let keyPathString = #keyPath(MyObject.myString)

The Swift docs list the type being passed into #keyPath() as a "property name".

"property name"

The property name must be a reference to a property that is available in the Objective-C runtime. At compile time, the key-path expression is replaced by a string literal.

Is it possible to save this "property name" independently, then later pass to #keyPath() to create a String?

let propertyName = MyObject.myString // error. How do I save?
let string = #keyPath(propertyName)

Is there any support for requiring a property name belonging to a specific Type?

// something like this
let typedPropertyName: MyObject.PropertyName = myString // error
let string = #keyPath(typedPropertyName)

The end goal will be interacting with with APIs that require an NSExpression for a key path. I would like to write convenience methods that take a valid Property Name as a parameter, rather than random key path strings. Ideally, a Property Name implemented by a specific Type.

func doSomethingForSpecificTypeProperty(_ propertyName: MyObject.PropertyName) {

    let keyPathString = #keyPath(propertyName)

    let expression = NSExpression(forKeyPath: keyPathString)

    // ...
}
Eikon answered 8/2, 2017 at 20:15 Comment(3)
Swift 4's Smart KeyPaths: Better Key-Value Coding for Swift seems to add a better option. Will accept that answer if anyone wants to write it up before I get to it.Eikon
bugs.swift.org/browse/SR-5220 - "Expose API to retrieve string representation of KeyPath"Eikon
github.com/kishikawakatsumi/Kuery - "A type-safe Core Data query API using Swift 4's Smart KeyPaths"Eikon
W
5

It doesn't look like it is possible.


Here's the compiler's code to parse a key path expression:

///   expr-keypath:
///     '#keyPath' '(' unqualified-name ('.' unqualified-name) * ')'
///
ParserResult<Expr> Parser::parseExprKeyPath() {
  // Consume '#keyPath'.
  SourceLoc keywordLoc = consumeToken(tok::pound_keyPath);

  // Parse the leading '('.
  if (!Tok.is(tok::l_paren)) {
    diagnose(Tok, diag::expr_keypath_expected_lparen);
    return makeParserError();
  }
  SourceLoc lParenLoc = consumeToken(tok::l_paren);

  // Handle code completion.
  SmallVector<Identifier, 4> names;
  SmallVector<SourceLoc, 4> nameLocs;
  auto handleCodeCompletion = [&](bool hasDot) -> ParserResult<Expr> {
    ObjCKeyPathExpr *expr = nullptr;
    if (!names.empty()) {
      expr = ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
                                     nameLocs, Tok.getLoc());
    }

    if (CodeCompletion)
      CodeCompletion->completeExprKeyPath(expr, hasDot);

    // Eat the code completion token because we handled it.
    consumeToken(tok::code_complete);
    return makeParserCodeCompletionResult(expr);
  };

  // Parse the sequence of unqualified-names.
  ParserStatus status;
  while (true) {
    // Handle code completion.
    if (Tok.is(tok::code_complete))
      return handleCodeCompletion(!names.empty());

    // Parse the next name.
    DeclNameLoc nameLoc;
    bool afterDot = !names.empty();
    auto name = parseUnqualifiedDeclName(
                  afterDot, nameLoc, 
                  diag::expr_keypath_expected_property_or_type);
    if (!name) {
      status.setIsParseError();
      break;
    }

    // Cannot use compound names here.
    if (name.isCompoundName()) {
      diagnose(nameLoc.getBaseNameLoc(), diag::expr_keypath_compound_name,
               name)
        .fixItReplace(nameLoc.getSourceRange(), name.getBaseName().str());
    }

    // Record the name we parsed.
    names.push_back(name.getBaseName());
    nameLocs.push_back(nameLoc.getBaseNameLoc());

    // Handle code completion.
    if (Tok.is(tok::code_complete))
      return handleCodeCompletion(false);

    // Parse the next period to continue the path.
    if (consumeIf(tok::period))
      continue;

    break;
  }

  // Parse the closing ')'.
  SourceLoc rParenLoc;
  if (status.isError()) {
    skipUntilDeclStmtRBrace(tok::r_paren);
    if (Tok.is(tok::r_paren))
      rParenLoc = consumeToken();
    else
      rParenLoc = PreviousLoc;
  } else {
    parseMatchingToken(tok::r_paren, rParenLoc,
                       diag::expr_keypath_expected_rparen, lParenLoc);
  }

  // If we cannot build a useful expression, just return an error
  // expression.
  if (names.empty() || status.isError()) {
    return makeParserResult<Expr>(
             new (Context) ErrorExpr(SourceRange(keywordLoc, rParenLoc)));
  }

  // We're done: create the key-path expression.
  return makeParserResult<Expr>(
           ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
                                   nameLocs, rParenLoc));
}

This code first creates a list of period-separated names inside the parentheses, and then it attempts to parse them as an expression. It accepts an expression and not data of any Swift type; it accepts code, not data.

Windbreak answered 8/2, 2017 at 20:36 Comment(1)
Hopefully this will be introduced in a future version of Swift!Eikon
R
1

Just came up with similar question and found this article. You can use KeyPath generic for these purposes

The short code for this in swift 4 looks like this:

let getName = \Person.name
print(p[keyPath: getName])

// or just this:
print(p[keyPath: \Person.name])
Regimentals answered 13/10, 2017 at 10:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.