2

I'm trying to create an instance of type B from an instance of type A, however some of the properties of the original type A are optional and the creation of B should throw an error if that occurs.

My issue is I can't tell if type T is optional, and if it is, how to unwrap it. I'm trying the following, but swift can't tell what the types should be...

The example below is contrite, the real sample has almost a hundred values.

struct A {
  let name: String?
  let price: Price?
  
  struct Price {
    let value: Double?
  }
}

struct B {
  let name: String
  let priceValue: Double
}

extension A {
  func convert() throws -> B {
    do {
      let name: String = try unwrap(\.name) // error: Type of expression is ambiguous without more context
      let priceValue: Double = try unwrap(\.price.value) // error: Type of expression is ambiguous without more context

      return B(name: name, priceValue: priceValue)
    }
  }

  func unwrap<U, T>(_ path: KeyPath<A, T>) throws -> U {

    let value = self[keyPath: path] // value is of type T

    if let value = value as? U {
      return value
    } else {
      throw Error.missing("KeyPath '\(path)' is 'nil'")
    }
  }

  enum Error: Swift.Error {
    case missing(String?)
  }
}

The following I know will work, but I'd rather not repeat this 100s of times in the code?


extension A {
  func convertWithConditionals() throws -> B {
    do {
      guard let name = self.name else {
        throw Error.missing("KeyPath 'name' is 'nil'")
      }

      guard let priceValue = self.price?.value else {
        throw Error.missing("KeyPath 'price.value' is 'nil'")
      }
     
      return B(name: name, priceValue: priceValue)
    }
  }
}

There must be some... swift-y way of doing this that I'm not thinking of.

Stephen Furlani
  • 6,794
  • 4
  • 31
  • 60

1 Answers1

3

If the intention is to call unwrap() only with key paths leading to an optional property then you can declare the argument type as KeyPath<A, T?>, and the second placeholder type U is not needed:

func unwrap<T>(_ path: KeyPath<A, T?>) throws -> T {
    if let value = self[keyPath: path] {
        return value
    } else {
        throw Error.missing("KeyPath '\(path)' is 'nil'")
    }
}

The usage can be simplified to

func convert() throws -> B {
    let name = try unwrap(\.name)
    return B(name: name)
}

or just

func convert() throws -> B {
    return try B(name: unwrap(\.name))
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • I still get `Type of expression is ambiguous without more context` as an error when I use your method, if the keyPath is more than 1 layer deep – Stephen Furlani Jan 07 '21 at 20:44
  • @StephenFurlani: So you have e.g. a keypath with 2 layers where *both* are optional? Try this: `let name = try unwrap(\.firstLayer?.name)` with the question mark after the first layer property. – Martin R Jan 07 '21 at 21:03
  • Beautiful! Thank you! I knew there must've been some gorgeous swifty way of doing it. – Stephen Furlani Jan 08 '21 at 13:27
  • @MartinR svg image position of uiimageview...We want get x,y position of image which is inside of UIImageview. Imageview is 0.3 percentage on mainscreen and leading, trailing, bottom are 0 and content mode is aspectfit. Now i want to add lable dynamically inside image. How to calculate this origin position Image gives only width and height – karthikeyan Jan 12 '21 at 09:14