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?
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.
Related
I got an error I do not understand, as I thought I understood unwrapping a conditional var/let. But when I try to force unwrap it in the if I get the supplied error.
Error:
Initializer for conditional binding must have Optional type, not 'String'
Code:
let imNotSet: String?
print(type(of: imNotSet)) // Optional<String>
if let unwrappedVar = imNotSet! { // error on this line
print(unwrappedVar)
}
if let unwrappedVar = imNotSet! { // error on this line
print(unwrappedVar)
}
imNotSet! forcefully unwrapped imNotSet. So it is no longer an optional but rather a string.
To keep it an optional, remove the forced unwrapping.
if let unwrappedVar = imNotSet { // error fixed
print(unwrappedVar)
}
if let allows you to safely unwrap the optional, unlike the forced unwrapping that you were doing before.
As for Constant 'imNotSet' used before being initialized error, Either provide it a value like let imNotSet: String? = "Sample", if it truly is a constant, before you use it. Or make it a var if you need to reset it later like var imNotSet: String? = nil
the var to be used with if let it must be an optional and this
imNotSet!
isn't , so replace
if let unwrappedVar = imNotSet! {
with
guard let unwrappedVar = imNotSet else { return }
So I am teaching myself Swift and I get optionals when I am declaring them, so for example:
var thisString:String?
I would have to either force unwrap thisString, or use
if let anotherString = thisString {
or use
guard if let another string = thisString else { return }
or nil coalesce it
let anotherString = thisString ?? "Something else"
But where I am getting hung up is there are times I create something that I don't think it an optional but the compiler does.
For example, why is the URL an optional here?
let myURL = URL(string: "https://itunes.apple.com/search?term=U2")
var myRequest = URLRequest(url: myURL!)
I didn't declare it as an optional and it clearly has a value. So why does the compiler see this as an optional? If I didn't force unwrap it I would get this error:
Value of optional type 'URL?' not unwrapped; did you mean to use '!' or '?'?
Is there some standard that I am missing? I've gone over both the docs and Swift Programming Book from Big Nerd and I still don't seem to get this part.
Thanks for the assist.
But where I am getting hung up is there are times I create something that I don't think it an optional but the compiler does.
But what you think is not what matters. What matters is what the API call you are making actually returns. You find that out by reading the docs.
So why does the compiler see this as an optional
Well, let's read the docs. You are calling URL(string:). That is the same as URL's init(string:). Look at the docs:
https://developer.apple.com/documentation/foundation/url/1779737-init
The declaration is
init?(string: String)
See the question mark? That means, "This call returns an Optional." So you should expect an Optional here.
The compiler can't determine if the url that you define in the string is valid or not.
Suppose instead of:
let myURL = URL(string: "https://itunes.apple.com/search?term=U2")
You miss typed the myURL definition as:
let myURL = URL(string: "https:/itunes.apple.com/search?term=U2")
The string contains a malformed URL, so the program would crash the moment you went to define myURL.
let myURL = URL(string: "https://itunes.apple.com/search?term=U2")
Here you are creating an url from string.That string might not be a valid string.All the strings are not valid url. So you are getting an optional because if that string can be turned in to a valid url then that url will be returned else nil will be returned. See the Apple documentation here.The initializer what you are using is a failable initializer itself.
init?(string: String)
#Keshav is correct, to get a better idea, hold the option button and click on the 'string' part of the init function for the URL class. You will see in the swift reference the init declaration is init?(string: String). This means that a optional is returned. Any function can return a optional, you can have func returnMyString(_ myString: String) -> String {} or func returnMyString(_ myString: String) -> String? {}. Both of those functions are pretty much the same, except the second one returns a optional.
URL has optional initializers. For example you can have
class A {
init?() {
return nil //You will get Optional<A>.none
}
}
A() === Optional<A>.none //true
which kind of implies that initialization failed. Such initializers wrap returned object into Optional. In Swift nil == Optional<Any>.none so you can speak of them interchangeably.
For example if you will attempt to construct a URL with something that is not an actual url, it will return nil.
let notAURL = URL(string: "{") //notAURL will be nil
On a side note: I believe optional initializers are a very poor design choice since they don't communicate anything about what went wrong inside the init. If init is fallible, it should throw. I don't understand why Swift designers allow optional initializers and I see why it births a lot of confusion.
I have a generic throwing method:
func doSomething<T>() throws -> T? {
// do something
}
This is how I want to call it:
let obj: SomeClass? = try? doSomething() // a compiler error!
But I may not. The compiler gives me an error
Value of optional type 'SomeClass??' not unwrapped; did you mean to use 'try!' or chain with '?'?.
I can use try! or try instead. But I don't like the former because it might crash the app at some point, and I don't like the latter because it's too long for most of the cases (one line becomes five).
Does anybody know how can I keep using try??
Your method is returning an optional. The try? expression adds another level of optional, therefore your expected return value should be a double optional:
let obj: SomeClass?? = try? doSomething()
It's probably not a good idea to combine returning of optional and throws.
You could also remove the second level of optionals using ??:
let obj: SomeClass? = (try? doSomething()) ?? nil
but I really recommend to redesign the API instead of solving double optionals.
I am trying to load a file from my app bundle in Swift 3, and I came across a weird situation with the Swift type inferencing. If I use the following code, I get an error on the third line that says Value of optional type "String?" not unwrapped.
let url = NSURL(fileURLWithPath:Bundle.main.bundlePath)
let url2 = url.appendingPathComponent("foo.txt")
let path:String = url2?.path
To fix the error I unwrap the value on the third line by changing it to:
let path:String = url2?.path!
I now get the error Cannot force unwrap value of a non-optional type 'String'. It seems like Swift can't determine whether the path property is a String or a String?. The autocomplete feature in Xcode says it is a String, but the docs say it is a String?.
The suggested fix by Xcode for the first error was to replace url2?.path with (url2?.path)!, which finally ended up working, but I have no idea why this works and the other ways don't.
let path:String = (url2?.path)!
What is going on? Is this a type inference bug in Swift, or am I missing something super obvious
In Swift, Optional chaining like:
let path:String = url2?.path!
... is interpreted as:
let path:String = url2 != nil ? url2!.path!
: nil
As you see the type of path is non-Optional String, so the expression causes error.
(url2's type is URL?, so the type of property path is String, not String?.)
This is not a direct answer to your question, but I would re-write your code as:
let url = Bundle.main.bundleURL
let url2 = url.appendingPathComponent("foo.txt")
let path:String = url2.path
Shorter, and no worry about Optionals.
You forgot to unwrap url2
appendingPathComponent returns an optional value and you are trying to access it without unwrapping it.
So,
let url2 = url.appendingPathComponent("foo.txt")!
or
guard let url2 = url.appendingPathComponent("foo.txt") else { }
should fix it
EDIT
let path:String? = url2?.path
works also
You can also do this:
let url = Bundle.main.url(forResource: "foo", withExtension: "txt")!
let path:String = url.path
I found this question which should have helped me, but the solution there is not working for me, and I am not sure if something has changed or if the problem is with my code.
let messageBody = "hello"
let urlSafeBody = messageBody.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet())
print("URLSAFEBODY: \(urlSafeBody)")
WKExtension.sharedExtension().openSystemURL(NSURL(string: "sms:&body=\(urlSafeBody)")!)
When this code is executed, I get the message that optional urlSafeBody was force unwrapped while nil, leading to a crash. Why is urlSafeBody nil? I know that I'm force unwrapping it, but I don't understand why it is ever nil after being assigned explicitly.
It's not urlSafeBody that is nil. As you can see from your print statement, it contains an optional string:
URLSAFEBODY: Optional("hello")
That will actually turn out to be a problem for the chain in the next statement, since you haven't unwrapped that string before it is interpolated.
If you examined your NSURL string URL, you'd see it contained:
sms:&body=Optional("hello")
This is going to cause NSURL initialization to fail, because its string URL is malformed. The fatal error then happens because you force-unwrapped the nil result from NSURL(string:)
How to resolve this:
You want to conditionally unwrap any strings which might be nil. You can do this via if let or guard let optional binding:
let messageBody = "hello"
let urlSafeBody = messageBody.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet())
if let urlSafeBody = urlSafeBody, url = NSURL(string: "sms:&body=\(urlSafeBody)") {
WKExtension.sharedExtension().openSystemURL(url)
}
Notice that the urlSafeBody was unwrapped before it was used in the string interpolation, and url was also unwrapped after initialization.
Since it's certain that the url is not nil, it can be safely passed to openSystemURL.
You should always strive to avoid force-unwrapping variables which may be nil, as that will certainly lead to a crash.