I used code below to copy a list of files to another place, It works
for (NSInteger i = 0;i<= urlArray1.count - 1;i++ ){
NSURL url1=[self urlForBookmark:[urlArray1 objectAtIndex:i];
NSURL url2=[self urlForBookmark:[urlArray2 objectAtIndex:i];
[[NSFileManager defaultManager] copyItemAtURL:url1 toURL:url2 error:&e];
}
labelA:
what I hope to get is when the code completed all files copy, something will be triggered.
But it look likes the code works in Async mode which means that labelA will be triggered immediately,
even the copy procedure is still working.
your comment welcome
Actually, its a synchronous operation, if you go to the docs:
Copies the file at the specified URL to a new location synchronously.
and returns a boolean with the result of the operation.
Related
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 was sending a file to the trash with
[NSWorkSpace recycleURLs: myArrayOfOneNsurl
completionHandler: NIL]
myArrayOfOneNsurl is a NSArray created with arrayWithObject: of a single NSURL that was created for an absolute file path with fileURLWithPath:isDirectory:.
The normal way to tell if it is successful is to use the completionHandler and then check if the 2nd arg (NSError) is NIL, which means success.
Is there anyway to check success without this callback? Currently I have setup a loop after calling this, to check for file existence, if 1second has past and it still exists, I declare fail. I was wondering if I set NIL as second arg to recycleURLs:completionHandler: does it make it block until the process completes (regardless of success)? In my tests, the very first check always finds that the file is no longer at its original place (meaning it was trashed), but I'm not sure if my computer is just super fast, or it really is blocking until file operation completes.
For trashing a single file or directory, you can use NSFileManager trashItemAtURL instead. It is synchronous, so you avoid the headaches with the completion callback.
NSError * error = nil;
[[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:nil error:&error];
This API is async. Passing NULL as completionHandler only means, that you are not interested in result. The only way to know when operation finished and was it successful is using completionHandler.
If you really need to make it sync, you may use following approach (although I don't recommend that):
__block BOOL recycleFinished = NO;
__block NSError *recycleError = nil;
[[NSWorkspace sharedWorkspace] recycleURLs:myArrayOfOneNsurl
completionHandler:^(NSDictionary<NSURL *,NSURL *> * _Nonnull newURLs, NSError * _Nullable error) {
recycleFinished = YES;
recycleError = error;
}];
while (!recycleFinished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:.5]];
}
// access recycleError
NSLog(#"%#", recycleError);
I have a method to save an image to the documents directory. Looks like this:
+(void)saveImageInDocumentsDirectory:(UIImage *)image withImageName:(NSString *)name {
NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString * basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
NSData * binaryImageData = UIImagePNGRepresentation(image);
[binaryImageData writeToFile:[basePath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.png",name]] atomically:YES];
}
Is there a way to add a completion block to this, that will run when the image is saved?
I don't think that there is a completion block. According to the docs, the return value indicates
Return Value
YES if the operation succeeds, otherwise NO.
To me this means that the method would block the thread it is called from until it is done.
If your saving is taking some time, you could use Grand Central Dispatch to execute the operation in a separate thread. You could call a method to be used as completion block when your method returns. Look for dispatch_async and you should find a lot.
There are also tons of examples here on SO. For example:
Dealing with Blocks, completion handlers, dispatch_async vs dispatch_sync
Hope this helps!
You could add an additional parameter to your method that represents the completion block. However, there is no point. None of the code involved here is asynchronous. When you call this saveImageInDocumentsDirectory:withImageName: method, the write is already complete when the method returns. So adding a support for a completion block gains you nothing.
So instead of adding support for a completion block and making a call like this:
[Whatever saveImageInDocumentsDirectory:someImage withImageName:#"SomeName" completion:^{
// some completion code
}];
You just need to do this:
[Whatever saveImageInDocumentsDirectory:someImage withImageName:#"SomeName"];
// some completion code here
I'm working on a RSS Reader using this tutorial. All table cells data come from a NSMutableArray instance (_allEntries). Then I import
EGOTableViewPullRefresh and add [self refresh] in -(void)reloadTableViewDataSource (self.refresh is a method to populate data of allEntries).
Then pull to refresh works but cells got duplicated every time I refresh. I tried to solve it in two ways.
When download data from internet, add if (![_allEntries containsObject:entry]) before [_allEntries insertObject:entry atIndex:insertIdx] but it didn't work, maybe I should use entry.title or some other attribute in the object to compare but it's not effective.
Like what I did in -viewDidLoad, add self.allEntries = [NSMutableArray array], but I don't know where should I put this line.
Is there anyone who can give me a direction?
[EDIT]
There's no too much logic in viewDidLoad, just
self.allEntries = [NSMutableArray array];
self.queue = [[NSOperationQueue alloc] init]; //add download&parse operation to a queue
self.feeds = [self getFeeds]; //load feeds from local file
And I put [self refresh] in reloadTableViewDataSource, the first time I open my app, there's nothing showed in the tableview. Then I pull to refresh, it works. Then pull to refresh again, it got duplicated.This is the "refresh" method.
- (void)refresh {
for (NSString *feed in _feeds) {
NSURL *url = [NSURL URLWithString:feed];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[_queue addOperation:request];
}
}
I want to rebuild the array so I write self.allEntries = [NSMutableArray array] again but it turns out "Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (140)". So as mentioned, I really get confused about where should I put this line.Thx~~
The logic you have in viewDidLoad that builds your array should be moved to its own method (reloadTableViewData), and then you would just call that method in viewDidLoad.
[self reloadTableViewData];
You would also call that same method when you do the pull to refresh.
Make sure you are rebuilding that array and not just adding objects to the existing one.
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.