0

In a singleton class I am trying the following code with 3 URLs stored in a dictionary:

class DownloadManager {
    static let instance = DownloadManager()
    
    let urls = [
        "en" : URL(string: "https://wordsbyfarber.com/ws/top"),
        "de" : URL(string: "https://wortefarbers.de/ws/top"),
        "ru" : URL(string: "https://slova.de/ws/top")
    ]
    
    var cancellables = Set<AnyCancellable>()

    private init() {
        getTops()
    }
    
    func getTops() {
        guard let url = urls["en"] else { return }
        
        URLSession.shared.dataTaskPublisher(for: url) // COMPILE ERROR
            .tryMap(handleOutput)
            .decode(type: TopResponse.self, decoder: JSONDecoder())
            .sink { completion in
                print(completion)
            } receiveValue: { fetchedTops in
                // ... store entities in Core Data
            }
            .store(in: &cancellables)
    }

But for some reason the line guard let url = urls["en"] else { return } is not sufficient to unwrap the value:

screenshot

Is this happening because the URL constructor might return nil?

Or because of the dictionary might not have a value for a key?

And why is guard statement not enough here?

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416

3 Answers3

2

urls is actually of type [String: URL?]. Note that the value type is optional, because URL.init(string:) is failable.

When you try to get a value from this dictionary, you get a URL??. The guard only unwraps one layer of the optional.

One way to unwrap a nested optional (no matter how many layers), is to use as?:

guard let url = urls["en"] as? URL else { return }
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Ah, there are "layers of optionality"? I am still learning Swift – Alexander Farber Jun 13 '21 at 10:24
  • 1
    @AlexanderFarber Yes, you can have `URL?`, `URL??`, `URL???` etc. You can add arbitrarily many question marks at the end. For `URL??`, there are 3 cases, it can be totally nil (`.none`), or the outer optional is not nil but the inner optional is nil (`.some(.none)`), or there can be an actual URL in there (`.some(.some(http://example.com))`). – Sweeper Jun 13 '21 at 10:28
  • 1
    @AlexanderFarber These cases correspond to "the dictionary lookup failed", "URL's initialiser failed but the dictionary lookup succeeded" and "both succeeded" respectively. – Sweeper Jun 13 '21 at 10:30
  • 1
    @AlexanderFarber There's nothing special about optionals. You can nest them the same way you can nest arrays or dictionaries with other arrys or dictionaries. – Alexander Jun 13 '21 at 13:41
1

Is this happening because the URL constructor might return nil?

Yes.

let urls = [
    "en" : URL(string: "https://wordsbyfarber.com/ws/top"),
    "de" : URL(string: "https://wortefarbers.de/ws/top"),
    "ru" : URL(string: "https://slova.de/ws/top")
]

creates a dictionary with optional URLs ([String: URL?]), and your unwrapping only relates to the contents of the dictionary.

Use

let urls = [
    "en" : URL(string: "https://wordsbyfarber.com/ws/top")!,
    "de" : URL(string: "https://wortefarbers.de/ws/top")!,
    "ru" : URL(string: "https://slova.de/ws/top")!
]

for hardcoded URLs like this, or double-unwrap if it's unclear if the URLs are valid.

Gereon
  • 17,258
  • 4
  • 42
  • 73
1

Thats as Url initializer returns an optional + the optional wrap of the Dictionary , If you're sure that all urls are valid then

 guard let url = urls["en"] else { return } 
 URLSession.shared.dataTaskPublisher(for: url!) // add only !

or only

 URLSession.shared.dataTaskPublisher(for: urls["en"]!!)   // add !!
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • 1
    @AlexanderFarber probably, but it would get confusing for both you and someone else if they are reading your code later – aheze Jun 13 '21 at 15:48