iProgram
iProgram

Reputation: 6577

Unexpectedly found nil while unwrapping an Optional value (AppleScript result)

I am trying to make a program in Swift 2 that runs and gets the result of an AppleScript script.

Here is my code:

import Foundation

func runAppleScript(script:String) -> String
{
    let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>()
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
    let theResult:String = theDiscriptor.stringValue! //This is whats causing the error

    return theResult
}

let scriptResult = runAppleScript("tell app \"Spotify\" to playpause")

NSLog("\(scriptResult)")

The problem is the program crashes and outputs:

fatal error: unexpectedly found nil while unwrapping an Optional value

in the console. I have also tried if let else, however that does not work either. How would I fix this issue?

This was tested using a OS X Command Line template using the swift language.

Upvotes: 1

Views: 234

Answers (2)

Eric Aya
Eric Aya

Reputation: 70118

Actually the error could come from NSAppleScript(source: script)! so the proper solution is to return an Optional String and not use force unwrapping at all:

func runAppleScript(script:String) -> String? {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo)
    return theDescriptor?.stringValue
}

if let scriptResult = runAppleScript("tell app \"Spotify\" to playpause") {
    NSLog("\(scriptResult)")
} else {
    print("the script execution failed")
}

If you prefer having a default value instead of nil when it fails, then no need to return an Optional:

func runAppleScript(script:String) -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo)
    return theDescriptor?.stringValue ?? ""  // if nil, returns the default ""
}

let scriptResult = runAppleScript("tell app \"Spotify\" to playpause")
NSLog("\(scriptResult)")

As for using the new Swift 2 error handling system, none of the methods you're using inside runAppleScript are throwing errors, so it would only work if you used a custom error type and throw the errors yourself. Example:

enum MyAppleScriptError: ErrorType {
    case ExecutingScriptFailed
    case GettingStringValueFailed
}

func runAppleScript(script:String) throws -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    guard let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo) else {
        throw MyAppleScriptError.ExecutingScriptFailed
    }
    guard let value = theDescriptor.stringValue else {
        throw MyAppleScriptError.GettingStringValueFailed
    }
    return value
}

do {
    let scriptResult = try runAppleScript("tell app \"Spotify\" to playpause")
    NSLog("\(scriptResult)")
} catch {
    print(error)
}

Swift 3

Same idea, but some implementation details are different.

func runAppleScript(_ script:String) -> String? {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?>? = nil
    if let startAtLoginScript = NSAppleScript(source: script) {
        let theDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
        return theDescriptor.stringValue
    }
    return nil
}

if let scriptResult = runAppleScript("tell app \"Spotify\" to playpause") {
    NSLog("\(scriptResult)")
} else {
    print("no return value")
}

And with error handling:

enum MyAppleScriptError: ErrorProtocol {
    case ExecutingScriptFailed
    case GettingStringValueFailed
}

func runAppleScript(_ script:String) throws -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?>? = nil
    let startAtLoginScript = NSAppleScript(source: script)
    guard let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo) else {
        throw MyAppleScriptError.ExecutingScriptFailed
    }
    guard let value = theDescriptor.stringValue else {
        throw MyAppleScriptError.GettingStringValueFailed
    }
    return value
}

do {
    let scriptResult = try runAppleScript("tell app \"Spotify\" to playpause")
    NSLog("\(scriptResult)")
} catch {
    print(error)
}

Upvotes: 1

iProgram
iProgram

Reputation: 6577

I have fixed my own code.

import Foundation

func runAppleScript(script:String) -> String
{
    let theResult:String
    let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>()
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
    if let _ = theDiscriptor.stringValue
    {
        theResult = theDiscriptor.stringValue!
    } else {
        theResult = ""
    }

    return theResult
}



let scriptResult = runAppleScript("")

What I had to do was check if theDiscriptor.stringValue has a value before unwrapping it. The error that I was getting is because I was trying to check the value after I had unwrapped it. Simply removing the ! on the check fixed my problem.

Edit

When trying this in Swift 3, the code let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>() no longer works. To fix this, I have updated the code.

func runAppleScript(script:String) -> String?
{
    var theResult:String?
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    var errorInfo:NSDictionary? = nil
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(&errorInfo)
    if let _ = theDiscriptor.stringValue {theResult = theDiscriptor.stringValue!}
    return theResult
}

Bonus

By returning an optional string, it allows you to check if the code returned a value.

Example:

Old way

let output = runAppleScript("script")
if output != ""
{
    //Script returned date
} else {
    //Script did not return data
}

New way

if let output = runAppleScript("script")
{
    //Script returned data
} else {
    //Script did not return data
}

Upvotes: 0

Related Questions