I have an NSDocument based application that uses filewrappers to save and load its data.
The document can have all kinds of resources, so I don't want to load everything into memory. I might be doing something fundamentally wrong, but as soon as I change one (inner) file and then save, I can't read any file that hasn't been loaded into memory.
I have separated the relevant code into a separate project to reproduce this behaviour, and I get the same results. The basic flow is this:
I load an existing document from disk. The main fileWrapper is a directory filewrapper (I'll call that main) containing two other filewrappers (sub1 and sub2). The two inner filewrappers are not loaded at this point.
When the user wants to edit sub1, it is loaded from disk.
The user saves the document
If the user wants to edit the other file (sub2), it cannot load. The error that appears:
-[NSFileWrapper regularFileContents] tried to read the file wrapper's contents lazily but an error occurred: The file couldn’t be opened because it doesn’t exist.
Here is the relevant code in my project:
This code might be easier to read in this gist:
https://gist.github.com/bob-codingdutchmen/6869871
#define FileName01 #"testfile1.txt"
#define FileName02 #"testfile2.txt"
/**
* Only called when initializing a NEW document
*/
-(id)initWithType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
self = [self init];
if (self) {
self.myWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
NSLog(#"Initializing new document...");
NSString *testString1 = #"Lorem ipsum first sub file";
NSString *testString2 = #"This is the second sub file with completely unrelated contents";
NSFileWrapper *w1 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString1 dataUsingEncoding:NSUTF8StringEncoding]];
NSFileWrapper *w2 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString2 dataUsingEncoding:NSUTF8StringEncoding]];
w1.preferredFilename = FileName01;
w2.preferredFilename = FileName02;
[self.myWrapper addFileWrapper:w1];
[self.myWrapper addFileWrapper:w2];
}
return self;
}
-(NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
// This obviously wouldn't happen here normally, but it illustrates
// how the contents of the first file would be replaced
NSFileWrapper *w1 = [self.myWrapper.fileWrappers objectForKey:FileName01];
[self.myWrapper removeFileWrapper:w1];
NSFileWrapper *new1 = [[NSFileWrapper alloc] initRegularFileWithContents:[#"New file contents" dataUsingEncoding:NSUTF8StringEncoding]];
new1.preferredFilename = FileName01;
[self.myWrapper addFileWrapper:new1];
return self.myWrapper;
}
-(BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
self.myWrapper = fileWrapper;
return YES;
}
- (IBAction)button1Pressed:(id)sender {
// Read from file1 and show result in field1
NSFileWrapper *w1 = [[self.myWrapper fileWrappers] objectForKey:FileName01];
NSString *string1 = [[NSString alloc] initWithData:w1.regularFileContents encoding:NSUTF8StringEncoding];
[self.field1 setStringValue:string1];
}
- (IBAction)button2Pressed:(id)sender {
// Read from file2 and show result in field2
NSFileWrapper *w2 = [[self.myWrapper fileWrappers] objectForKey:FileName02];
NSString *string2 = [[NSString alloc] initWithData:w2.regularFileContents encoding:NSUTF8StringEncoding];
[self.field2 setStringValue:string2];
}
The bottom two methods are only for updating the UI so I can see what happens.
To change the contents of a file, I remove the existing fileWrapper and add a new one. This is the only way I've found to change the contents of a file, and the way I've seen it done in other SO answers.
When a document is loaded from disk, I keep the fileWrapper around so I can use it (called myWrapper in the code above)
The Apple docs say that NSFileWrapper supports lazy loading and incremental saving, so I'm assuming that my code has some fundamental flaw that I can't see.
An NSFileWrapper is essentially a wrapper around a unix file node. If the file is moved the wrapper stays valid.
The problem yo seem to have is that creating a new file wrapper during saving is a new folder. And the system deletes your previous wrapper including sub2.
To achieve what you want you need to change to incremental saving, i.e. Only saving changed parts in place. See "save in place" in NSDocument.
In your -fileWrapperOfType:error: method, try building a new file wrapper that has new contents for the changed members and references the old file wrappers for the unchanged members.
Following the documentation to addFileWrapper: you add a child (subdirectory) to it, means
directory/
addfileWrapper:fileName1
directory/fileName1/
addfileWrapper:fileName2
directory/fileName1/fileName2.
That file doesn't exist.
You have to use
addRegularFileWithContents:preferredFilename:
instead.
Related
I am seeking help to understand why a tutorial I am following is not working for me. I am running macOS 12.3.1, Xcode 13.3.1. The project is in Objective-C and using XIB.
This is a view-based NSTableView, using a folder of PNGs stored on my SSD for the imageView and the stringByDeletingPathExtension as stringValue for the cell's text field. I filled my code with NSLog calls to try and catch what could have been going awry.
Most setup is happening in applicationDidFinishLaunching:, where I initialise an NSMutableArray for the table's content, an NSString for the file path, then set up the file manager and the directory enumerator with said path (note: all working up to here).
Now comes the loop to populate the table contents' mutable array. I cannot understand why said loop gets skipped entirely! Its condition is to set an NSString equal to the nextObject of the directory enumerator. I am sure the loop gets skipped because the NSLog call after the loop runs!
Here is the entire code of applicationDidFinishLaunching:, including my comments and logs (I have just replaced my account name with ):
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
_tableContents = [[NSMutableArray alloc] init];
NSString *path = #"/Users/<myUsername>/Developer/Apple-Programming-YT/Cocoa Programming/Flags/PNG/40x30";
// MARK: Debug 1
NSLog(#"path found: %#", path); // the correct path gets printed, as expected
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDirectoryEnumerator *directoryEnum = [fileManager enumeratorAtPath:path];
NSString *file;
// MARK: Debug 2
NSLog(#"Checking that file is empty: %#", file); // (null) gets printed, as expected
// MARK: Debug 3
if (file != directoryEnum.nextObject) {
NSLog(#"File cannot be assigned to the Directory Enumerator");
} else if (file == directoryEnum.nextObject) {
NSLog(#"File properly assigned. Proceed!"); // this gets printed! Is it correct?
} else {
NSLog(#"Something went wrong during assignment of nextObject to file");
}
while (file = [directoryEnum nextObject]) {
NSLog(#"While loop entered!"); // this doesn't get printed! Why?!
// MARK: Debug 4
NSLog(#"File: %#", file);
NSString *filePath = [path stringByAppendingFormat:#"/%#", file];
// MARK: Debug 5
NSLog(#"Image filepath: %#", filePath);
NSDictionary *obj = #{#"image": [[NSImage alloc] initByReferencingFile:filePath],
#"name": [file stringByDeletingPathExtension]};
[self.tableContents addObject:obj];
}
[self.tableView reloadData];
NSLog(#"Table View Reloaded"); // This gets printed!
}
I have uploaded the full app to GitHub, in case you may want to look at it and see if something else could be wrong, but every outlet, delegate, data source is connected.
Now for my diagnosis & ideas:
The Debug 3 mark is what I find most interesting. AFAIK file should still be (null), so how checking if it is equal to directoryEnum.nextObject returns YES?
I created Debug 3 because the NSLog checking whether the loop had been entered didn't get printed. I therefore assumed the condition for the while loop had a problem.
I then tried to create a do-while loop instead of this while loop and, of course, the code ran. For the log with "Image filepath" it returned the address above followed by (null), as if it didn't find the file. But how is it possible if the file is indeed there? Do I require some sort of permission to access it? Being the object empty, the next line in the console was quite clear: "attempt to insert nil object from objects[1]".
But now, how do I solve this?
Any help here is much appreciated. If you download it from GitHub, please replaces the *path string with a folder of PNGs on your SSD.
Thank you.
I don't think you can access the filesystem directly with a path like that any more. If you check the value of file in your code, it is nil, which means that file == directoryEnum.nextObject will evaluate to true.
You have to create a path starting with NSHomeDirectory() or similar and add components to it. This makes a path that goes via your application support folder, which contains an alias to the desktop. I'm not sure why that's OK and accessing it directly is not, but I'm not a Mac developer.
I'd have to say following a tutorial as old as that, you're going to struggle with a lot of things.
My application would like to add a promise to the pasteboard for a file that is stored remotely, and may never be pasted—similar to pasting a file copied from a session controlling a VM or other remote system. Ideally, a user can paste in a Finder folder (or the desktop) and the promise would trigger and away we go. I am willing to deal with the issues of fulfilling the promise once triggered, but I have been unable to get the promise to trigger.
All of the promise code I have found deals with drag and drop, which is not functionality what I need (though it is possible that something from DnD needs to be in place for promises to work?)
I have tried using NSFilePromiseProvider with a delegate, and adding that to the pasteboard. I can see the entries on the pasteboard using a clipboard viewer, but when I paste in Finder nothing happens and no delegate methods are called. I can trigger the delegate methods by having the clipboard viewer access the entries, so I know that much is hooked up.
#interface ClipboardMacPromise : NSFilePromiseProvider<NSFilePromiseProviderDelegate>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFileType:(NSString*)type andFile:(NSString*)file
{
m_file = file;
return [super initWithFileType:type delegate:self];
}
- (NSString *)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider fileNameForType:(NSString *)fileType
{
return m_file;
}
- (void)filePromiseProvider:(NSFilePromiseProvider*)filePromiseProvider writePromiseToURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable errorOrNil))completionHandler
{
// Finder can't paste, so we never get here...
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFileType:(NSString*)kUTTypeFileURL andFile:#"dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
I have also tried NSPasteboardItem with NSPasteboardItemDataProvider where I setup a promise for content on kUTITypeFileURL. It provided very similar entries on the pasteboard, but still no action when I paste in finder. Clipboard viewer will again trigger the provider fine when accessing the individual pasteboard entries. (NSPasteboard's declareTypes:owner: has the same behavior)
#interface ClipboardMacPromise : NSPasteboardItem<NSPasteboardItemDataProvider>
{
NSString* m_file;
}
#end
#implementation ClipboardMacPromise
- (id)initWithFile:(NSString*)file
{
m_file = file;
id _self = [super init];
if (_self) {
[_self setDataProvider:_self forTypes:#[(NSString*)kPasteboardTypeFileURLPromise]];
[_self setString:(NSString*)kUTTypeFileURL forType:(NSString*)kPasteboardTypeFilePromiseContent];
}
return _self;
}
- (void)pasteboard:(NSPasteboard *)pasteboard item:(NSPasteboardItem *)item provideDataForType:(NSPasteboardType)type
{
// we don't get here when we paste in Finder because
// Finder doesn't think there's anything to paste
// but using a clipboard viewer, we can force the promise to
// resolve and we do get here
}
#end
NSPasteboard* pboard = [NSPasteboard generalPasteboard];
[pboard clearContents];
NSMutableArray* items = [[NSMutableArray alloc] init];
ClipboardMacPromise* promise = [[ClipboardMacPromise alloc] initWithFile:#"file:///tmp/dummy.txt"];
[items addObject:promise];
[pboard writeObjects:items];
And for completeness, here is my Carbon attempt since Pasteboard.h seems to detail how this should work in a copy/paste scenario... but it still does not provide Finder what it is looking for. The generated clipboard entries look very similar between the three implementations.
OSStatus PasteboardPromiseKeeperProc(PasteboardRef pasteboard, PasteboardItemID item, CFStringRef flavorType, void * _Nullable context)
{
// 6) The sender's promise callback for kPasteboardTypeFileURLPromise is called.
string s = "dummy.txt";
CFDataRef inData = CFDataCreate(kCFAllocatorDefault, (UInt8*)s.c_str(), s.size());
PasteboardPutItemFlavor(pasteboard, item, flavorType, inData, 0);
return noErr;
}
PasteboardRef p = NULL;
PasteboardCreate(kPasteboardClipboard, &p);
PasteboardClear(p);
PasteboardSetPromiseKeeper(p, &PasteboardPromiseKeeperProc, this);
// 1) The sender promises kPasteboardTypeFileURLPromise for a file yet to be created.
PasteboardPutItemFlavor(p, (PasteboardItemID)1, kPasteboardTypeFileURLPromise, kPasteboardPromisedData, 0);
// 2) The sender adds kPasteboardTypeFilePromiseContent containing the UTI describing the file's content.
PasteboardPutItemFlavor(p, (PasteboardItemID)2, kPasteboardTypeFilePromiseContent,CFStringCreateExternalRepresentation(NULL, kUTTypeFileURL, kCFStringEncodingUTF8, 0), 0);
It really seems that there is a certain UTI that Finder is looking for on the pasteboard, and I don't have it. If I put a kUTTypeFileURL directly on the clipboard, it appears that finder actually checks for the existence of the file (ie. triggers Catalina's Desktop access prompt) before offering it to paste.
Does anyone know if or how file promises can be provided to Finder through Copy/Paste instead of Drag-and-Drop?
It appears that the key piece here is that Finder requires that the file actually be present on disk for the paste action to be enabled for a file URL. This one detail rules out the possibility of promises working for copy/paste -- at least with Finder.
The correct solution therefore requires a virtualized file system (like FUSE) so that the promises can be made and fulfilled at the filesystem level. Thus a collection of temporary zero-length files can be written to disk, and actual file URLs be added to the pasteboard. This fulfills the requirements that Finder has to enable paste. Then when a paste action is made, the file data is read from the virtualized file system which can in turn retrieve the actual data from the remote system. Finder is none the wiser. The copy will even have a built in progress bar!
It appears that Microsoft's Mac RDP client mostly works this way, although I was only ever able to get it to copy zero length files so this may be harder to get right than it sounds.
I'm trying to write a simple (toy) program that uses the NSFilePresenter and NSFileCoordinator methods to watch a file for changes.
The program consists of a text view that loads a (hardcoded) text file and a button that will save the file with any changes. The idea is that I have two instances running and saving in one instance will cause the other instance to reload the changed file.
Loading and saving the file works fine but the NSFilePresenter methods are never called. It is all based around a class called FileManager which implements the NSFilePresenter protocol. The code is as follows:
Interface:
#interface FileManager : NSObject <NSFilePresenter>
#property (unsafe_unretained) IBOutlet NSTextView *textView;
- (void) saveFile;
- (void) reloadFile;
#end
Implementation:
#implementation FileManager
{
NSOperationQueue* queue;
NSURL* fileURL;
}
- (id) init {
self = [super init];
if (self) {
self->queue = [NSOperationQueue new];
self->fileURL = [NSURL URLWithString:#"/Users/Jonathan/file.txt"];
[NSFileCoordinator addFilePresenter:self];
}
return self;
}
- (NSURL*) presentedItemURL {
NSLog(#"presentedItemURL");
return self->fileURL;
}
- (NSOperationQueue*) presentedItemOperationQueue {
NSLog(#"presentedItemOperationQueue");
return self->queue;
}
- (void) saveFile {
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
[coordinator coordinateWritingItemAtURL:self->fileURL options:NSFileCoordinatorWritingForMerging error:&error byAccessor:^(NSURL* url) {
NSString* content = [self.textView string];
[content writeToFile:[url path] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
}];
}
- (void) reloadFile {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
__block NSData* content;
[coordinator coordinateReadingItemAtURL:self->fileURL options:0 error:&error byAccessor:^(NSURL* url) {
if ([fileManager fileExistsAtPath:[url path]]) {
content = [fileManager contentsAtPath:[url path]];
}
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self.textView setString:[[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]];
});
}
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
#end
Note, reloadFile is called in applicationDidFinishLaunching and saveFile gets called every time the save button is click (via the app delegate).
The only NSFilePresenter method that ever gets called (going by the logs) is presentedItemURL (which gets called four times when the program starts and loads the file and three times whenever save is clicked. Clicking save in a second instance has no noticeable effect on the first instance.
Can anyone tell me what I'm doing wrong here?
I was struggling with this exact issue for quite a while. For me, the only method that would be called was -presentedSubitemDidChangeAtURL: (I was monitoring a directory rather than a file). I opened a technical support issue with Apple, and their response was that this is a bug, and the only thing we can do right now is to do everything through -presentedSubitemDidChangeAtURL: if you're monitoring a directory. Not sure what can be done when monitoring a file.
I would encourage anyone encountering this issue to file a bug (https://bugreport.apple.com) to encourage Apple to get this problem fixed as soon as possible.
(I realize that this is an old question, but... :) )
First of all, I notice you don't have [NSFileCoordinator removeFilePresenter:self]; anywhere (it should be in dealloc).
Secondly, you wrote:
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
You're right: it's the incorrect implementation! And you're wrong: it's not good enough, because it's essential for methods like accommodatePresentedItemDeletionWithCompletionHandler: which take a completion block as a parameter, that you actually call this completion block whenever you implement them, e.g.
- (void) savePresentedItemChangesWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler
{
// implement your save routine here, but only if you need to!
if ( dataHasChanged ) [self save]; // <-- meta code
//
NSError * err = nil; // <-- = no error, in this simple implementation
completionHandler(err); // <-- essential!
}
I don't know whether this is the reason your protocol methods are not being called, but it's certainly a place to start. Well, assuming you haven't already worked out what was wrong in the past three years! :-)
I've noticed some weird behavior with NSBundle when using it in a
command-line program. If, in my program, I take an existing bundle and
make a copy of it and then try to use pathForResource to look up
something in the Resources folder, nil is always returned unless the
bundle I'm looking up existed before my program started. I created a
sample app that replicates the issue and the relevant code is:
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *exePath = [NSString stringWithCString:argv[0]
encoding:NSASCIIStringEncoding];
NSString *path = [exePath stringByDeletingLastPathComponent];
NSString *templatePath = [path stringByAppendingPathComponent:#"TestApp.app"];
// This call works because TestApp.app exists before this program is run
NSString *resourcePath = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:templatePath];
NSLog(#"NOCOPY: %#", resourcePath);
NSString *copyPath = [path stringByAppendingPathComponent:#"TestAppCopy.app"];
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:templatePath
toPath:copyPath
error:nil])
{
// This call will fail if TestAppCopy.app does not exist before
// this program is run
NSString *resourcePath2 = [NSBundle pathForResource:#"InfoPlist"
ofType:#"strings"
inDirectory:copyPath];
NSLog(#"COPY: %#", resourcePath2);
[[NSFileManager defaultManager] removeItemAtPath:copyPath
error:nil];
}
[pool release];
}
For the purpose of this test app, let's assume that TestApp.app
already exists in the same directory as my test app. If I run this,
the 2nd NSLog call will output: COPY: (null)
Now, if I comment out the final removeItemAtPath call in the if
statement so that when my program exits TestAppCopy.app still exists
and then re-run, the program will work as expected.
I've tried this in a normal Cocoa application and I can't reproduce
the behavior. It only happens in a shell tool target.
Can anyone think of a reason why this is failing?
BTW: I'm trying this on 10.6.4 and I haven't tried on any other
versions of Mac OS X.
I can confirm that it is a bug in CoreFoundation, not Foundation. The bug is due to CFBundle code relying on a directory contents cache containing stale data. The code apparently assumes that neither the bundle directories nor their immediate parent directories will change during application runtime.
The CoreFoundation call corresponding to +[NSBundle pathForResource:ofType:inDirectory:] is CFBundleCopyResourceURLInDirectory(), and it exhibits the same misbehavior. (This is unsurprising, as -pathForResource:ofType:inDirectory: itself uses this call.)
The problem ultimately lies with _CFBundleCopyDirectoryContentsAtPath(). This is called during bundle loading and during all resource lookup. It caches information about the directories it looks up in contentsCache.
Here's the problem: When it comes time to get the contents of TestAppCopy.app, the cached contents of the directory containing TestApp.app don't include TestAppCopy.app. Because the cache ostensibly has the contents of that directory, only the cached contents are searched for TestAppCopy.app. When TestAppCopy.app is not found, the function takes that as a definitive "this path does not exist" and doesn't bother trying to open the directory:
__CFSpinLock(&CFBundleResourceGlobalDataLock);
if (contentsCache) dirDirContents = (CFArrayRef)CFDictionaryGetValue(contentsCache, dirName);
if (dirDirContents) {
Boolean foundIt = false;
CFIndex dirDirIdx, dirDirLength = CFArrayGetCount(dirDirContents);
for (dirDirIdx = 0; !foundIt && dirDirIdx < dirDirLength; dirDirIdx++) if (kCFCompareEqualTo == CFStringCompare(name, CFArrayGetValueAtIndex(dirDirContents, dirDirIdx), kCFCompareCaseInsensitive)) foundIt = true;
if (!foundIt) tryToOpen = false;
}
__CFSpinUnlock(&CFBundleResourceGlobalDataLock);
So, the contents array remains empty, gets cached for this path, and lookup continues. We now have cached the (incorrectly empty) contents of TestAppCopy.app, and as lookup drills down into this directory, we keep hitting bad cached information. Language lookup takes a stab when it finds nothing and hopes there's an en.lproj hanging around, but we still won't find anything, because we're looking in a stale cache.
CoreFoundation includes SPI functions to flush the CFBundle caches. The only place public API calls into them in CoreFoundation is __CFBundleDeallocate(). This flushes all cached information about the bundle's directory itself, but not its parent directory: _CFBundleFlushContentsCacheForPath(), which actually removes the data from the cache, removes only keys matching an anchored, case-insensitive search for the bundle path.
It would seem the only public way a client of CoreFoundation could flush bad information about TestApp.app's parent directory would be to make the parent directory a bundle directory (so TestApp.app lived alongside Contents), create a CFBundle for the parent bundle directory, then release that CFBundle. But, it seems that if you made the mistake of trying to work with the TestAppCopy.app bundle prior to flushing it, the bad data about TestAppCopy.app would not be flushed.
That sounds like a bug in the Foundation. The one key difference between a command line tool like that one and a Cocoa application is the run loop. Try refactoring the above into something like:
#interface Foo:NSObject
#end
#implementation Foo
- (void) doIt { .... your code from main() here .... }
#end
... main(...) {
Foo *f = [Foo new];
[f performSelector: #selector(doIt) withObject: nil afterDelay: 0.1 ...];
[[NSRunLoop currentRunLoop] run];
return 0; // not reached, I'd bet.
}
And see if that "fixes" it. It might. It might not (there are couple of other significant differences, obviously). In any case, do please file a bug via http://bugreport.apple.com/ and add the bug # as a comment.
Is there anyway to do Files Handling in Objective-C? I am just trying to do simple read and write and can use 'c' but i am force to use Objective-C classes for that :#. I am looking into NSInputStream, but its going over my head. Is there any tutorial which explains how to use NSInputStream?
I had trouble with basic file i/o when I first hit it in Obj-C as well. I ended up using NSFileHandle to get C style access to my file. Here's a basic example:
// note: myFilename is an NSString containing the full path to the file
// create the file
NSFileManager *fManager = [[NSFileManager alloc] init];
if ([fManager createFileAtPath:myFilename contents:nil attributes:nil] != YES) {
NSLog(#"Failed to create file: %#", myFilename);
}
[fManager release]; fManager = nil;
// open the file for updating
NSFileHandle *myFile = [NSFileHandle fileHandleForUpdatingAtPath:myFilename];
if (myFile == nil) {
NSLog(#"Failed to open file for updating: %#", myFilename);
}
// truncate the file so it is guaranteed to be empty
[myFile truncateFileAtOffset:0];
// note: rawData is an NSData object
// write data to a file
[myFile writeData:rawData];
// close the file handle
[myFile closeFile]; myFile = nil;
If all you need to do is really simple I/O, you can just tell an object to initialize itself from, or write itself to, a filesystem path or URL. This works with several Foundation classes, including NSString, NSData, NSArray, and NSDictionary among others.
Try starting out by looking at the following two NSString methods:
- initWithContentsOfFile:encoding:error:
- writeToFile:atomically:encoding:error:
I find apple's guides short and to the point.
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html