How to properly handle NSFileHandle exceptions in Swift 2.0?
Asked Answered
L

3

23

First of all, I am new to iOS and Swift and come from a background of Android/Java programming. So to me the idea of catching an exception from an attempt to write to a file is second nature, in case of lack of space, file permissions problems, or whatever else can possibly happen to a file (and has happened, in my experience). I also understand that in Swift, exceptions are different from Android/Java ones, so that's not what I'm asking about here.

I am attempting to append to a file using NSFileHandle, like so:

let fileHandle: NSFileHandle? = NSFileHandle(forUpdatingAtPath: filename)
if fileHandle == nil {
    //print message showing failure
} else {
    let fileString = "print me to file"
    let data = fileString.dataUsingEncoding(NSUTF8StringEncoding)
    fileHandle?.seekToEndOfFile() 
    fileHandle?.writeData(data!)
}

However, both the seekToEndOfFile(), and writeData() functions indicate that they throw some kind of exception:

This method raises an exception if the file descriptor is closed or is not valid, if the receiver represents an unconnected pipe or socket endpoint, if no free space is left on the file system, or if any other writing error occurs. - Apple Documentation for writeData()

So what is the proper way to handle this in Swift 2.0? I've read the links Error-Handling in Swift-Language, try-catch exceptions in Swift, NSFileHandle writeData: exception handling, Swift 2.0 exception handling, and How to catch an exception in Swift, but none of them have a direct answer to my question. I did read something about using objective-C in Swift code, but since I am new to iOS, I don't know what this method is and can't seem to find it anywhere. I also tried the new Swift 2.0 do-catch blocks, but they don't recognize that any type of error is being thrown for NSFileHandle methods, most likely since the function documentation has no throw keyword.

I am aware that I could just let the app crash if it runs out of space or whatever, but since the app will possibly be released to the app store later, I don't want that. So how do I do this the Swift 2.0 way?

EDIT: This currently is a project with only Swift code, so even though it seems there is a way to do this in Objective-C, I have no idea how to blend the two.

Looksee answered 22/1, 2016 at 21:9 Comment(0)
L
40

a second (recoverable) solution would be to create a very simple ObjectiveC++ function that takes a block and returns an exception.

create a file entitled: ExceptionCatcher.h and add import it in your bridging header (Xcode will prompt to create one for you if you don't have one already)

//
//  ExceptionCatcher.h
//

#import <Foundation/Foundation.h>

NS_INLINE NSException * _Nullable tryBlock(void(^_Nonnull tryBlock)(void)) {
    @try {
        tryBlock();
    }
    @catch (NSException *exception) {
        return exception;
    }
    return nil;
}

Using this helper is quite simple, I have adapted my code from above to use it.

func appendString(string: String, filename: String) -> Bool {
    guard let fileHandle = NSFileHandle(forUpdatingAtPath: filename) else { return false }
    guard let data = string.dataUsingEncoding(NSUTF8StringEncoding) else { return false }

    // will cause seekToEndOfFile to throw an excpetion
    fileHandle.closeFile()

    let exception = tryBlock {
        fileHandle.seekToEndOfFile()
        fileHandle.writeData(data)
    }
    print("exception: \(exception)")

    return exception == nil
}
Leading answered 25/1, 2016 at 21:52 Comment(7)
Unfortunately I can't get this to compile. Xcode (version 7.2) gives me the error Unknown type name 'NS_ASSUME_NONNULL_BEGIN' and of course the same for the matching end declaration. Supposedly this was fixed in Xcode 6.4 from what I'm reading, and since I'm not using any pods or anything, I have no idea why I have this error. Any ideas?Looksee
very odd, i have not seen this issue. try creating a new Swift project and use the code there. if that works find the diff between your two project files.Leading
updated code to stop using NS_ASSUME_NONNULL_BEGINLeading
I have very little experience with objective C - now it's saying Unknown type name 'NS_INLINE' and Expected ';' after top level declarator. I can't find a reason for this error anywhere online, so I'm wondering if it's a syntactical or compiling error on my end. I have nothing else in my .h file but what you have.Looksee
That was it. Works very well, thanks. I would give you +1 but I don't have enough rep. Accepted, though.Looksee
This saved my day. Thank you muchly for this very nicely implemented solution to an unfortunate problem!Akkerman
Note, this does not catch all exceptions, even if they appear to be very similar in nature. i.e. it will consistently catch certain NSExceptions, while just as consistently NOT catch others. Never figured out why.Demo
L
4

This can be achieved without using Objective C code, here is a complete example.

class SomeClass: NSObject {
    static func appendString(string: String, filename: String) -> Bool {
        guard let fileHandle = NSFileHandle(forUpdatingAtPath: filename) else { return false }
        guard let data = string.dataUsingEncoding(NSUTF8StringEncoding) else { return false }

        // will cause seekToEndOfFile to throw an excpetion
        fileHandle.closeFile()

        SomeClass.startHandlingExceptions()
        fileHandle.seekToEndOfFile()
        fileHandle.writeData(data)
        SomeClass.stopHandlingExceptions()

        return true
    }

    static var existingHandler: (@convention(c) NSException -> Void)?
    static func startHandlingExceptions() {
        SomeClass.existingHandler = NSGetUncaughtExceptionHandler()
        NSSetUncaughtExceptionHandler({ exception in
            print("exception: \(exception))")
            SomeClass.existingHandler?(exception)
        })
    }

    static func stopHandlingExceptions() {
        NSSetUncaughtExceptionHandler(SomeClass.existingHandler)
        SomeClass.existingHandler = nil
    }
}

Call SomeClass.appendString("add me to file", filename:"/some/file/path.txt") to run it.

Leading answered 23/1, 2016 at 0:49 Comment(8)
With some changes, this does catch the exception like you said. The change I had to make includes the NSFileHandle - though what you have compiles, it always triggers the else part of guard when initializing fileHandle. It should be something like guard let fileHandle: NSFileHandle? = NSFileHandle(forUpdatingAtPath: filename) else { return false }, like in my code above. However, I cannot get the exception details to print, even if I change print to NSLog. Do you know why this is?Looksee
Also, it seems your code only catches the error, prints it (if I can get it to go to NSLog, as I mentioned), and then moves on. What if I wanted to know whether or not the handler caught something in my Swift code? Is there a swift way to do it or does that definitely need Objective C code?Looksee
i'm not sure why you had to change the guard statement, it should be fine as long as filename is a valid file path. this code will not be able to recover from the exception unfortunately, it just gives you a chance to see it before the app crashes.Leading
at this point I would recommend creating a small ObjC class that does a @try/@catch and returns the result back to swiftLeading
I'm not sure why either, I printed out the filename and everything and it was valid, it just returned false from the guard statement each time. But thank you for your suggestion. It looks like for my particular purpose, JAL's answer is more correct, as I do want to be able to warn the user about the exception somehow.Looksee
see alternate solution i posted, should be exactly what you need.Leading
Interesting solution, when was this added to Swift?Graffito
as far as i know it was always there, ties in to existing C functionLeading
G
1

seekToEndOfFile() and writeData() are not marked as throws (they don't throw an NSError object which can be caught in with a do-try-catch block), which means in the current state of Swift, the NSExceptions raised by them cannot be "caught".

If you're working on a Swift project, you could create an Objective-C class which implements your NSFileHandle methods that catches the NSExceptions (like in this question), but otherwise you're out of luck.

Graffito answered 22/1, 2016 at 21:49 Comment(3)
Re @matt's answer: I respectfully disagree in the specific case of writeData(). An NSException could be raised if "if any ... writing error occurs". Maybe the user has a jailbroken system or no space left on their device. How are you supposed to account for every possible scenario that could raise an exception with NSFileHandle?Graffito
I've edited the question to show that indeed, I've been working on a Swift-only project. Could you provide an example or link on how I would incorporate an Objective-C class into the project, so I can handle the errors properly? The question you link to just shows Objective-C code with no context. I could always look it up, but for those who look at this question later it might be useful.Looksee
You would need to use a bridging header.Graffito

© 2022 - 2024 — McMap. All rights reserved.