"Unexpectedly found nil while unwrapping an Optional value" in URL

Before anyone rants and raves about checking documentation - I have spent the last 4 hours trying to solve this issue on my own before asking for help. Coding in Swift is VERY new for me and I'm banging my head against the wall trying to teach myself. I am very humbly asking for help. If you refer me to documentation, that's fine but I need examples or it's going to go right over my head. Teaching myself is hard, please don't make it more difficult.

I have ONE swift file with everything in it.

import Foundation
import Cocoa
import Observation

class GlobalString: ObservableObject {
    @Published var apiKey = ""
    @Published var link = ""
}

struct ContentView: View {
    @EnvironmentObject var globalString: GlobalString
    
    var body: some View {
        
        Form {
            Section(header: Text("WallTaker for macOS").font(.title)) {
                
                TextField(
                    "Link ID:",
                    text: $globalString.link
                )
                .disableAutocorrection(true)
                
                TextField(
                    "API Key:",
                    text: $globalString.apiKey
                )
                .disableAutocorrection(true)
                Button("Take My Wallpaper!") {
                }
                
            }
            .padding()
        }
        .task {
            await Wallpaper().fetchLink()
        }
    }
}

@main
struct WallTaker_for_macOSApp: App {
    @AppStorage("showMenuBarExtra") private var showMenuBarExtra = true
    @EnvironmentObject var globalString: GlobalString
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(GlobalString())
        }
//        MenuBarExtra("WallTaker for macOS", systemImage: "WarrenHead.png", isInserted: $showMenuBarExtra) {
//            Button("Refresh") {
////                currentNumber = "1"
//                }
//            Button("Love It!") {
////                currentNumber = "2"
//                }
//            Button("Hate It!") {
////                currentNumber = "3"
//                }
//            Button("EXPLOSION!") {
//    //                currentNumber = "3"
//                }
////
//        }
    }
}

class Wallpaper {
    var url: URL? = nil
    var lastPostUrl: URL? = nil
    let mainMonitor: NSScreen

    init() {
        mainMonitor = NSScreen.main!
    }
    
    struct LinkResponse: Codable {
        var post_url: String?
        var set_by: String?
        var updated_at: String
    }

    struct Link {
        var postUrl: URL?
        var setBy: String
        var updatedAt: Date
    }

    func parseIsoDate(timestamp: String) -> Date? {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        return formatter.date(from: timestamp)
    }


    func fetchLink() async {
        do {
            url = URL(string: GlobalString().link)
            let (data, _) = try await URLSession.shared.data(from: url!)
            let decoder = JSONDecoder()
            let linkResponse = try decoder.decode(LinkResponse.self, from: data)

            let postUrl: URL? = linkResponse.post_url != nil ? URL(string: linkResponse.post_url!) : nil

            let date = parseIsoDate(timestamp: linkResponse.updated_at)

            let link = Link(
                    postUrl: postUrl,
                    setBy: linkResponse.set_by ?? "anon",
                    updatedAt: date ?? Date()
            )
            try update(link: link)
        } catch {
        }
    }

    func update(link: Link) throws {
        guard let newPostUrl = link.postUrl else {
            return
        }

        if (newPostUrl != lastPostUrl) {
            lastPostUrl = newPostUrl

            let tempFilePath = try getTempFilePath()
            try downloadImageTo(sourceURL: newPostUrl, destinationURL: tempFilePath)
            try applyWallpaper(url: tempFilePath)
        } else {
        }
    }

    private func applyWallpaper(url: URL) throws {
        try NSWorkspace.shared.setDesktopImageURL(url, for: mainMonitor, options: [:])
    }

    private func getTempFilePath() throws -> URL {
        let directory = NSTemporaryDirectory()
        let fileName = NSUUID().uuidString

        let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName])!

        return fullURL
    }

    private func downloadImageTo(sourceURL: URL, destinationURL: URL) throws {
        let data = try Data(contentsOf: sourceURL)
        try data.write(to: destinationURL)
    }

}

The 'fetchLink' function is where things explode, specifically when setting the URL. I do not know what I'm doing wrong.

Answered by Developer Tools Engineer in 790300022

So, let's start with the error message. "Unexpectedly found nil while unwrapping an Optional value" means that you used the postfix ! operator to force-unwrap an optional, but the optional was nil. I'm guessing it's specifically complaining about the ! in this line:

            let (data, _) = try await URLSession.shared.data(from: url!)

So, let's start looking backwards from there. Where was url set? We find this on the line above:

url = URL(string: GlobalString().link)

So, this line must be setting url to nil. Why?

URL.init(string:) is a "failable initializer", meaning that it returns an optional URL. If you look at its documentation, you'll see that it says:

This initializer returns nil if the string doesn’t represent a valid URL even after encoding invalid characters.

So that suggests the problem is that GlobalString().link is returning a string that isn't a valid URL. To figure out what that string is and why you're getting it, it might make sense to break up that line into several steps:

let globalStringObject = GlobalString()
let linkFromGlobalStringObject = globalStringObject.link
url = URL(string: linkFromGlobalStringObject)

And then use your favorite debugging technique to look at the values of linkFromGlobalStringObject and globalStringObject. (You might set a breakpoint on the line with the URL(string:) call so you can inspect the value of the variable before you pass it, for instance, or you might log or print the variables.)

If you do, what I believe you'll find is that linkFromGlobalStringObject is an empty string (which, indeed, is not a valid URL!) and globalStringObject has empty apiKey and link properties. And if you read the code closely, that actually makes sense, because GlobalString() calls the initializer on the GlobalString class, and that initializer sets the apiKey and link fields to empty. You probably wanted to use the GlobalString object that's in the environment object, not create a brand new one!

Wallpaper isn't a SwiftUI view, so it can't access the environment object directly; instead, you'll have to pass the GlobalString object in from the view. I'd recommend adding a new property to Wallpaper and making the initializer take a value for it:

class Wallpaper {
    var url: URL? = nil
    var lastPostUrl: URL? = nil
    let mainMonitor: NSScreen
    let globalString: GlobalString    // *** NEW ***

    init(globalString: GlobalString) {    // *** CHANGED ***
        mainMonitor = NSScreen.main!
        self.globalString = globalString    // *** NEW ***
    }

    // ...intervening code should be unchanged...

    func fetchLink() async {
        do {
            url = URL(string: globalString.link)    // *** CHANGED ***

And then modify the view to pass its globalString in when it creates the Wallpaper object:

        .task {
            await Wallpaper(globalString: globalString).fetchLink()    // *** CHANGED ***
        }

Now, if you try this again…actually, it'll still crash, and the link field will still be an empty string. But you might notice that it crashed before you filled in the Link ID field. The problem is that .task calls fetchLink() as soon as the view loads, which isn't really what you wanted it to do! You wanted it to run fetchLink() when you clicked the Button, right?

So remove the whole .task { ... } modifier and instead, change the button's action like so:

                Button("Take My Wallpaper!") {
                    Task {    // *** NEW ***
                        await Wallpaper(globalString: globalString).fetchLink()    // *** NEW ***
                    }    // *** NEW ***
                }

Now it'll start fetching the link only once the button is pressed, so if you put a valid URL in the Link ID field and hit the button, it should successfully fetch the wallpaper (or at least it will get past the place where it crashed before).

But you might notice that fetchLink() does still crash if you leave the Link ID blank when you hit the button, or if you fill it in with garbage instead of a valid URL. Ultimately, it is normal and expected for URL.init(string:) to return nil when the URL is invalid, and you need to detect when that happens and handle it gracefully. Perhaps fetchLink() could throw an error or return a Bool so the view can detect that it needs to show an error message; perhaps you could disable the button until the Link ID is valid (to check that, you could speculatively call URL.init(string:) in the view and see if it gives you a non-nil result). There are a lot of different options, so I'll stop this post here and let you decide how to proceed.

If I understand, the error is here:

    func fetchLink() async {
        do {
            url = URL(string: GlobalString().link)
            let (data, _) = try await URLSession.shared.data(from: url!)

If you look at URL documentation, you see that URL(string: _) returns an optional which may be nil (that's what happens in your case)

init?(string: String)
Creates a URL instance from the provided string.

Why is it nil, that's what you have now to investigate.

To make your code robust, test for nil:

    func fetchLink() async {
        do {
             url = URL(string: GlobalString().link) 
             if url == nil {
               print(GlobalString())  // <<-- To understand what happens
               return // do not continue
            }
            let (data, _) = try await URLSession.shared.data(from: url!)     // <<-- cannot be nil anymore

Do the same by testing post_url if the error is here:

let postUrl: URL? = linkResponse.post_url != nil ? URL(string: linkResponse.post_url!) : nil

Note: In Swift we avoid using underscore and use camelCase instead. That means post_url should better be postUrl or postURL for instance.

At what point does GlobalString().link goes from an empty string to a valid URL before being used. The pattern around the usage of GlobalString is also incorrect.

@main
struct WallTaker_for_macOSApp: App {
    @AppStorage("showMenuBarExtra") private var showMenuBarExtra = true
    @StateObject var globalString = GlobalString()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(globalString)
        }
     }
}

Then change link from an empty string to a valid URL string to prevent the URL init from returning nil.

Also change from

 url = URL(string: GlobalString().link)

to

 url = URL(string: globalString.link)

from

 .task {
            await Wallpaper().fetchLink()
        }

to something like

 .task {
            let wallpaper = Wallpaper()
            wallpaper.url = URL(globalString.link)
            await wallpaper.fetchLink()
        }

Then change link from an empty string to a valid URL string to prevent the URL init from returning nil.

The string starts as nil until the end user supplies one. I'm struggling to prevent fetching until the link is valid. Do you have a suggestion for what to put there for the init that won't **** up for the end user?

URL(string:) is optional, meaning if the string can't be parsed into a URL, you will get nil back. Always guard the initializer to make sure the references that come later are non-nil. Also note, if nil is returned, it won't trigger your catch block since the initializer only returns nil and doesn't throw.

So, let's start with the error message. "Unexpectedly found nil while unwrapping an Optional value" means that you used the postfix ! operator to force-unwrap an optional, but the optional was nil. I'm guessing it's specifically complaining about the ! in this line:

            let (data, _) = try await URLSession.shared.data(from: url!)

So, let's start looking backwards from there. Where was url set? We find this on the line above:

url = URL(string: GlobalString().link)

So, this line must be setting url to nil. Why?

URL.init(string:) is a "failable initializer", meaning that it returns an optional URL. If you look at its documentation, you'll see that it says:

This initializer returns nil if the string doesn’t represent a valid URL even after encoding invalid characters.

So that suggests the problem is that GlobalString().link is returning a string that isn't a valid URL. To figure out what that string is and why you're getting it, it might make sense to break up that line into several steps:

let globalStringObject = GlobalString()
let linkFromGlobalStringObject = globalStringObject.link
url = URL(string: linkFromGlobalStringObject)

And then use your favorite debugging technique to look at the values of linkFromGlobalStringObject and globalStringObject. (You might set a breakpoint on the line with the URL(string:) call so you can inspect the value of the variable before you pass it, for instance, or you might log or print the variables.)

If you do, what I believe you'll find is that linkFromGlobalStringObject is an empty string (which, indeed, is not a valid URL!) and globalStringObject has empty apiKey and link properties. And if you read the code closely, that actually makes sense, because GlobalString() calls the initializer on the GlobalString class, and that initializer sets the apiKey and link fields to empty. You probably wanted to use the GlobalString object that's in the environment object, not create a brand new one!

Wallpaper isn't a SwiftUI view, so it can't access the environment object directly; instead, you'll have to pass the GlobalString object in from the view. I'd recommend adding a new property to Wallpaper and making the initializer take a value for it:

class Wallpaper {
    var url: URL? = nil
    var lastPostUrl: URL? = nil
    let mainMonitor: NSScreen
    let globalString: GlobalString    // *** NEW ***

    init(globalString: GlobalString) {    // *** CHANGED ***
        mainMonitor = NSScreen.main!
        self.globalString = globalString    // *** NEW ***
    }

    // ...intervening code should be unchanged...

    func fetchLink() async {
        do {
            url = URL(string: globalString.link)    // *** CHANGED ***

And then modify the view to pass its globalString in when it creates the Wallpaper object:

        .task {
            await Wallpaper(globalString: globalString).fetchLink()    // *** CHANGED ***
        }

Now, if you try this again…actually, it'll still crash, and the link field will still be an empty string. But you might notice that it crashed before you filled in the Link ID field. The problem is that .task calls fetchLink() as soon as the view loads, which isn't really what you wanted it to do! You wanted it to run fetchLink() when you clicked the Button, right?

So remove the whole .task { ... } modifier and instead, change the button's action like so:

                Button("Take My Wallpaper!") {
                    Task {    // *** NEW ***
                        await Wallpaper(globalString: globalString).fetchLink()    // *** NEW ***
                    }    // *** NEW ***
                }

Now it'll start fetching the link only once the button is pressed, so if you put a valid URL in the Link ID field and hit the button, it should successfully fetch the wallpaper (or at least it will get past the place where it crashed before).

But you might notice that fetchLink() does still crash if you leave the Link ID blank when you hit the button, or if you fill it in with garbage instead of a valid URL. Ultimately, it is normal and expected for URL.init(string:) to return nil when the URL is invalid, and you need to detect when that happens and handle it gracefully. Perhaps fetchLink() could throw an error or return a Bool so the view can detect that it needs to show an error message; perhaps you could disable the button until the Link ID is valid (to check that, you could speculatively call URL.init(string:) in the view and see if it gives you a non-nil result). There are a lot of different options, so I'll stop this post here and let you decide how to proceed.

"Unexpectedly found nil while unwrapping an Optional value" in URL
 
 
Q