7

My class has a property of type NSURL that is initialized from a string. The string is known at compile time.

For the class to operate appropriately, it must be set to its intended value at initialization (not later), so there is no point in defining it as an optional (implicitly unwrapped or otherwise):

class TestClass: NSObject {

    private let myURL:NSURL

    ...

Assuming that NSURL(string:) (which returns NSURL?) will never fail if passed a valid URL string that is known at compile time, I can do something like this:

    override init() {

        myURL = NSURL(string: "http://www.google.com")!

        super.init()
    }

However, I somehow don't feel comfortable around the forced unwrapping and would like to guard the URL initialization somehow. If I try this:

    guard myURL = NSURL(string: "http://www.google.com") else {
        fatalError()
    }

Value of optional type 'NSURL?' not unwrapped; did you mean to use '!' or '?'?

(Note: there's no way to add a ! or ? anywhere the code above that will fix the error. Conditional unwrapping only happens with guard let... guard var..., and myURL is already defined)

I understand why this fails: Even a successful call to NSURL(string:) is returning the (valid) NSURL wrapped inside an optional NSURL?, so I still need to unwrap it somehow before assigning to myURL (which is non-optional, hence not compatible for assignment as-is).

I can get around this by using an intermediate variable:

    guard let theURL = NSURL(string: "http://www.google.com") else {
        fatalError()
    }

    myURL = theURL

...but this is obviously not elegant at all.

What should I do?

Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189
  • If you want to use optional binding in this situation, there is no alternative except to use the extra variable. I think using an `if let` statement might be a bit more elegant, as it confines the extra variable to the scope of the `if` block. – Marc Khadpe Jan 15 '16 at 06:30
  • Also, if you're not concerned with dealing with an error, then you could use the nil coalescing operator: `myURL = NSURL(string: "http://www.google.com") ?? NSURL()`. – Marc Khadpe Jan 15 '16 at 06:34
  • If you expect that NSURL(string:) does not fail and you want your program to abort if it does (unexpectedly), then it makes no difference if you use forced unwrapping or guard+fatalError. – Martin R Jan 15 '16 at 06:42
  • About unwrapping directly into properties, see http://stackoverflow.com/questions/31271241/guard-when-setting-multiple-class-properties-in-swift-2. – Martin R Jan 15 '16 at 07:48
  • "You can’t unwrap into existing variables. Instead, you have to unwrap, then assign". I guess that settles it. – Nicolas Miari Jan 15 '16 at 07:58
  • 1
    I'm seriously considering marking my question as a duplicate of that one. – Nicolas Miari Jan 15 '16 at 08:00
  • 1
    Thank you @LeoDabus, I fixed it. – Nicolas Miari Jan 15 '16 at 08:01

1 Answers1

6

Update Another approach, that doesn't use guard, would be to use a switch, as optionals map to the Optional enum:

init?() {
    switch URL(string: "http://www.google.com") {
    case .none:
        myURL = NSURL()
        return nil
    case let .some(url):
        myURL = url
    }
}

although you'd still get a url local variable.


Original answer

You can declare your initializer as a failable one and return nil in case the url string parsing fails, instead of throwing a fatal error. This will make it more clear to clients your the class that the initializer might fail at some point. You still won't get rid of the guard, though.

init?() {

    guard let url = URL(string: "http:www.google.com") else {
        // need to set a dummy value due to a limitation of the Swift compiler
        myURL = URL()
        return nil
    }
    myURL = url

}

This add a little complexity on the caller side, as it will need to check if the object creation succeeded, but it's the recommended pattern in case the object initializer can fail constructing the object. You'd also need to give up the NSObject inheritance as you cannot override the init with a failable version (init?).

You can find out more details about failable initializers on the Swift blog, Apple's documentation, or this SO question.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • or keep the inheritance and add to the failable initializer a parameter, for example the url string: `init?(url: String) {...}` – TPlet Jan 15 '16 at 06:56
  • I already know that code (it's in my question). This part: `// need to set a dummy value due to a limitation of the Swift compiler`, I guess this is the (negative) answer to my question? – Nicolas Miari Jan 15 '16 at 07:29
  • So, there's no `guard`ing without an intermediate variable? – Nicolas Miari Jan 15 '16 at 07:30
  • 1
    I don't think so, you need to have a `let` declaration to unbind, which creates a local constant, see also Martin R's comment – Cristik Jan 15 '16 at 07:55
  • @NicolasMiari I added a `switch`-based solution to my answer, maybe you'll like that better then the `guard` one :) – Cristik Jan 15 '16 at 15:38
  • Interesting. I'm not looking for a failable initializer, but the "switch case none:" is something I didn't know. Thanks! – Nicolas Miari Jan 15 '16 at 15:43
  • @NicolasMiari optionals are mapped by the compiler to the `Optional` enum, this is why you can use the `switch` statement – Cristik Jan 15 '16 at 15:48
  • 1
    Requiring a dummy value for properties when returning nil in an initializer is a compiler bug. I believe it has been fixed in the next release of Swift. – rsmoz Jan 16 '16 at 03:36