20

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.

Community
  • 1
  • 1
mirage
  • 313
  • 2
  • 8

3 Answers3

32

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
}
Casey
  • 5,618
  • 20
  • 40
  • 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? – mirage Jan 25 '16 at 23:07
  • 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. – Casey Jan 25 '16 at 23:09
  • updated code to stop using ```NS_ASSUME_NONNULL_BEGIN``` – Casey Jan 25 '16 at 23:14
  • 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. – mirage Jan 26 '16 at 00:35
  • That was it. Works very well, thanks. I would give you +1 but I don't have enough rep. Accepted, though. – mirage Jan 26 '16 at 01:35
  • This saved my day. Thank you muchly for this very nicely implemented solution to an unfortunate problem! – devios1 Feb 01 '16 at 23:10
  • 2
    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. – David James Jul 28 '16 at 16:57
2

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.

Casey
  • 5,618
  • 20
  • 40
  • 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? – mirage Jan 25 '16 at 20:29
  • 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? – mirage Jan 25 '16 at 20:52
  • 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. – Casey Jan 25 '16 at 21:20
  • at this point I would recommend creating a small ObjC class that does a @try/@catch and returns the result back to swift – Casey Jan 25 '16 at 21:20
  • 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. – mirage Jan 25 '16 at 21:36
  • see alternate solution i posted, should be exactly what you need. – Casey Jan 25 '16 at 21:52
  • Interesting solution, when was this added to Swift? – JAL Jan 25 '16 at 23:16
  • as far as i know it was always there, ties in to existing C function – Casey Jan 25 '16 at 23:18
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.

Community
  • 1
  • 1
JAL
  • 39,073
  • 22
  • 153
  • 285
  • 1
    Re @matt's answer: I respectfully disagree in the specific case of `writeData()`. An `NSException` could be raised if ["if any ... writing error occurs"](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSFileHandle_Class/#//apple_ref/occ/instm/NSFileHandle/writeData:). 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`? – JAL Jan 22 '16 at 21:56
  • 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. – mirage Jan 22 '16 at 22:35
  • 1
    You would need to use a [bridging header](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html). – JAL Jan 22 '16 at 23:32